├── .devcontainer └── devcontainer.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── dependabot.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── abi ├── aave_flashloan_abi.json ├── aave_pool_abi.json ├── erc20_abi.json ├── gas_price_oracle_abi.json ├── sushiswap_abi.json └── uniswap_abi.json ├── contracts ├── IERC20.sol └── SimpleFlashloan.sol ├── docs └── mermaid1.svg ├── linear_regression ├── price_model.joblib └── training_data.csv ├── python ├── __init__.py ├── abiregistry.py ├── apiconfig.py ├── app.py ├── configuration.py ├── loggingconfig.py ├── main.py ├── maincore.py ├── marketmonitor.py ├── mempoolmonitor.py ├── noncecore.py ├── pyutils │ ├── __init__.py │ └── strategyexecutionerror.py ├── safetynet.py ├── strategynet.py └── transactioncore.py ├── requirements.txt ├── template.env ├── tests ├── test_abiregistry.py ├── test_apiconfig.py ├── test_configuration.py ├── test_init.py ├── test_loggingconfig.py ├── test_main.py ├── test_maincore.py ├── test_marketmonitor.py ├── test_mempoolmonitor.py ├── test_noncecore.py ├── test_safetynet.py ├── test_strategyexecutionerror.py └── test_transactioncore.py ├── ui └── index.html └── utils ├── erc20_signatures.json ├── token_addresses.json └── token_symbols.json /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ON1Builder Dev Container", 3 | "image": "mcr.microsoft.com/devcontainers/python:3.12", // Updated to Python 3.12 4 | "features": { 5 | "ghcr.io/devcontainers/features/python:1": { 6 | "version": "3.12" 7 | } 8 | }, 9 | "postCreateCommand": "pip install -r requirements.txt && pytest tests/", 10 | "customizations": { 11 | "vscode": { 12 | "extensions": [ 13 | "ms-python.python", // Python extension 14 | "ms-python.vscode-pylance", // Pylance for IntelliSense 15 | "ms-toolsai.jupyter", // Jupyter support 16 | "ms-python.black-formatter", // Black formatter 17 | "ms-python.flake8", // Flake8 linter 18 | "ms-python.isort" // isort for imports 19 | ], 20 | "settings": { 21 | "python.formatting.provider": "black", 22 | "python.linting.flake8Enabled": true, 23 | "python.linting.enabled": true, 24 | "editor.formatOnSave": true 25 | } 26 | } 27 | }, 28 | "workspaceFolder": "/workspace/ON1Builder" // Set default workspace folder 29 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Dependabot - ON1Builder 2 | version: 2 3 | updates: 4 | - package-ecosystem: "pip" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | target-branch: "master" 9 | allow: 10 | - dependency-name: "*" 11 | dependency-type: "direct" 12 | assignees: 13 | - "John0n1" 14 | 15 | reviewers: 16 | - "John0n1" 17 | 18 | commit-message: 19 | prefix: "chore: " 20 | prefix-development: "chore(dev): " 21 | include: "scope" 22 | open-pull-requests-limit: 8 23 | rebase-strategy: "auto" 24 | versioning-strategy: "increase-if-necessary" 25 | milestone: 1 26 | groups: 27 | core: 28 | patterns: 29 | - "django" 30 | update-types: 31 | - "minor" 32 | - "patch" 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Node modules 3 | node_modules/ 4 | 5 | # Logs 6 | logs/ 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | 12 | # Runtime data 13 | pids/ 14 | *.pid 15 | *.seed 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage/ 19 | 20 | # Environment variable files 21 | .env 22 | .env.local 23 | .env.development 24 | .env.test 25 | .env.production 26 | venv 27 | .venv 28 | # Build output 29 | dist/ 30 | build/ 31 | __pycache__ 32 | 33 | # OS files 34 | .DS_Store 35 | Thumbs.db 36 | 37 | # IDE directories 38 | .vscode/ 39 | .idea/ 40 | 41 | # Private directories 42 | private/ 43 | 44 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to ON1Builder MEV Bot 2 | 3 | ## Quick Start 4 | 5 | 1. **Fork & Clone** 6 | ```bash 7 | git clone https://github.com/John0n1/ON1Builder.git 8 | cd ON1Builder 9 | ``` 10 | 11 | 2. **Set Up** 12 | ```bash 13 | python3 -m venv venv 14 | source venv/bin/activate # Windows: venv\Scripts\activate 15 | pip install -r requirements.txt 16 | ``` 17 | 18 | 3. **Configure** 19 | ```bash 20 | cp .env.example .env 21 | # Fill in your API keys 22 | ``` 23 | 24 | ## What You Need 25 | - Python 3.12+ 26 | - Git 27 | - Ethereum node access 28 | - Some API keys (Infura, Etherscan, etc.) 29 | - A test wallet 30 | 31 | ## How to Help 32 | 33 | ### Found a Bug? 34 | Open an issue! Tell us: 35 | - What happened 36 | - What should have happened 37 | - Steps to reproduce 38 | 39 | ### Have an Idea? 40 | Open an issue and tell us about your feature suggestion. 41 | 42 | ### Want to Code? 43 | 44 | 1. Create a branch 45 | ```bash 46 | git checkout -b feature/cool-new-thing 47 | ``` 48 | 49 | 2. Code away! 50 | - Keep it simple 51 | - Add tests 52 | - Update docs if needed 53 | 54 | 3. Submit a PR 55 | - Clear title and description 56 | - Link related issues 57 | 58 | ## Style Guide 📝 59 | 60 | - Follow PEP 8 61 | - Use type hints 62 | - Max 88 chars per line 63 | - Clear variable names 64 | 65 | ## Testing 66 | 67 | ```bash 68 | pytest tests/ 69 | ``` 70 | 71 | ## Questions? 72 | 73 | - Open an issue 74 | - Join our community discussions 75 | 76 | ## License 77 | 78 | MIT License 79 | 80 | --- 81 | 82 | Remember: Keep it simple, have fun, and happy coding! 83 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 John Mitander 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 | -------------------------------------------------------------------------------- /abi/aave_flashloan_abi.json: -------------------------------------------------------------------------------- 1 | [{"inputs":[{"internalType":"address","name":"_addressProvider","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"token","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"premium","type":"uint256"},{"indexed":false,"internalType":"bool","name":"success","type":"bool"}],"name":"FlashLoanExecuted","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"token","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"FlashLoanRequested","type":"event"},{"inputs":[],"name":"ADDRESSES_PROVIDER","outputs":[{"internalType":"contract IPoolAddressesProvider","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"POOL","outputs":[{"internalType":"contract IPool","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"asset","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"uint256","name":"premium","type":"uint256"},{"internalType":"address","name":"initiator","type":"address"},{"internalType":"bytes","name":"params","type":"bytes"}],"name":"executeOperation","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"uint256","name":"_amount","type":"uint256"}],"name":"fn_RequestFlashLoan","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address payable","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"withdrawETH","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_tokenAddress","type":"address"}],"name":"withdrawToken","outputs":[],"stateMutability":"nonpayable","type":"function"},{"stateMutability":"payable","type":"receive"}] 2 | -------------------------------------------------------------------------------- /abi/aave_pool_abi.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { 5 | "internalType": "address", 6 | "name": "admin", 7 | "type": "address" 8 | } 9 | ], 10 | "stateMutability": "nonpayable", 11 | "type": "constructor" 12 | }, 13 | { 14 | "anonymous": false, 15 | "inputs": [ 16 | { 17 | "indexed": true, 18 | "internalType": "address", 19 | "name": "implementation", 20 | "type": "address" 21 | } 22 | ], 23 | "name": "Upgraded", 24 | "type": "event" 25 | }, 26 | { 27 | "stateMutability": "payable", 28 | "type": "fallback" 29 | }, 30 | { 31 | "inputs": [], 32 | "name": "admin", 33 | "outputs": [ 34 | { 35 | "internalType": "address", 36 | "name": "", 37 | "type": "address" 38 | } 39 | ], 40 | "stateMutability": "view", 41 | "type": "function" 42 | }, 43 | { 44 | "inputs": [], 45 | "name": "implementation", 46 | "outputs": [ 47 | { 48 | "internalType": "address", 49 | "name": "", 50 | "type": "address" 51 | } 52 | ], 53 | "stateMutability": "view", 54 | "type": "function" 55 | }, 56 | { 57 | "inputs": [ 58 | { 59 | "internalType": "address", 60 | "name": "_logic", 61 | "type": "address" 62 | }, 63 | { 64 | "internalType": "bytes", 65 | "name": "_data", 66 | "type": "bytes" 67 | } 68 | ], 69 | "name": "initialize", 70 | "outputs": [], 71 | "stateMutability": "payable", 72 | "type": "function" 73 | }, 74 | { 75 | "inputs": [ 76 | { 77 | "internalType": "address", 78 | "name": "newImplementation", 79 | "type": "address" 80 | } 81 | ], 82 | "name": "upgradeTo", 83 | "outputs": [], 84 | "stateMutability": "nonpayable", 85 | "type": "function" 86 | }, 87 | { 88 | "inputs": [ 89 | { 90 | "internalType": "address", 91 | "name": "newImplementation", 92 | "type": "address" 93 | }, 94 | { 95 | "internalType": "bytes", 96 | "name": "data", 97 | "type": "bytes" 98 | } 99 | ], 100 | "name": "upgradeToAndCall", 101 | "outputs": [], 102 | "stateMutability": "payable", 103 | "type": "function" 104 | } 105 | ] 106 | -------------------------------------------------------------------------------- /abi/erc20_abi.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [], 4 | "name": "name", 5 | "outputs": [ 6 | { 7 | "internalType": "string", 8 | "name": "", 9 | "type": "string" 10 | } 11 | ], 12 | "stateMutability": "view", 13 | "type": "function" 14 | }, 15 | { 16 | "inputs": [ 17 | { 18 | "internalType": "address", 19 | "name": "_spender", 20 | "type": "address" 21 | }, 22 | { 23 | "internalType": "uint256", 24 | "name": "_value", 25 | "type": "uint256" 26 | } 27 | ], 28 | "name": "approve", 29 | "outputs": [ 30 | { 31 | "internalType": "bool", 32 | "name": "", 33 | "type": "bool" 34 | } 35 | ], 36 | "stateMutability": "nonpayable", 37 | "type": "function" 38 | }, 39 | { 40 | "inputs": [], 41 | "name": "totalSupply", 42 | "outputs": [ 43 | { 44 | "internalType": "uint256", 45 | "name": "", 46 | "type": "uint256" 47 | } 48 | ], 49 | "stateMutability": "view", 50 | "type": "function" 51 | }, 52 | { 53 | "inputs": [ 54 | { 55 | "internalType": "address", 56 | "name": "_from", 57 | "type": "address" 58 | }, 59 | { 60 | "internalType": "address", 61 | "name": "_to", 62 | "type": "address" 63 | }, 64 | { 65 | "internalType": "uint256", 66 | "name": "_value", 67 | "type": "uint256" 68 | } 69 | ], 70 | "name": "transferFrom", 71 | "outputs": [ 72 | { 73 | "internalType": "bool", 74 | "name": "", 75 | "type": "bool" 76 | } 77 | ], 78 | "stateMutability": "nonpayable", 79 | "type": "function" 80 | }, 81 | { 82 | "inputs": [], 83 | "name": "decimals", 84 | "outputs": [ 85 | { 86 | "internalType": "uint8", 87 | "name": "", 88 | "type": "uint8" 89 | } 90 | ], 91 | "stateMutability": "view", 92 | "type": "function" 93 | }, 94 | { 95 | "inputs": [ 96 | { 97 | "internalType": "address", 98 | "name": "_owner", 99 | "type": "address" 100 | } 101 | ], 102 | "name": "balanceOf", 103 | "outputs": [ 104 | { 105 | "internalType": "uint256", 106 | "name": "balance", 107 | "type": "uint256" 108 | } 109 | ], 110 | "stateMutability": "view", 111 | "type": "function" 112 | }, 113 | { 114 | "inputs": [], 115 | "name": "symbol", 116 | "outputs": [ 117 | { 118 | "internalType": "string", 119 | "name": "", 120 | "type": "string" 121 | } 122 | ], 123 | "stateMutability": "view", 124 | "type": "function" 125 | }, 126 | { 127 | "inputs": [ 128 | { 129 | "internalType": "address", 130 | "name": "_to", 131 | "type": "address" 132 | }, 133 | { 134 | "internalType": "uint256", 135 | "name": "_value", 136 | "type": "uint256" 137 | } 138 | ], 139 | "name": "transfer", 140 | "outputs": [ 141 | { 142 | "internalType": "bool", 143 | "name": "", 144 | "type": "bool" 145 | } 146 | ], 147 | "stateMutability": "nonpayable", 148 | "type": "function" 149 | }, 150 | { 151 | "inputs": [ 152 | { 153 | "internalType": "address", 154 | "name": "_owner", 155 | "type": "address" 156 | }, 157 | { 158 | "internalType": "address", 159 | "name": "_spender", 160 | "type": "address" 161 | } 162 | ], 163 | "name": "allowance", 164 | "outputs": [ 165 | { 166 | "internalType": "uint256", 167 | "name": "", 168 | "type": "uint256" 169 | } 170 | ], 171 | "stateMutability": "view", 172 | "type": "function" 173 | }, 174 | { 175 | "stateMutability": "payable", 176 | "type": "fallback" 177 | }, 178 | { 179 | "anonymous": false, 180 | "inputs": [ 181 | { 182 | "indexed": true, 183 | "internalType": "address", 184 | "name": "owner", 185 | "type": "address" 186 | }, 187 | { 188 | "indexed": true, 189 | "internalType": "address", 190 | "name": "spender", 191 | "type": "address" 192 | }, 193 | { 194 | "indexed": false, 195 | "internalType": "uint256", 196 | "name": "value", 197 | "type": "uint256" 198 | } 199 | ], 200 | "name": "Approval", 201 | "type": "event" 202 | }, 203 | { 204 | "anonymous": false, 205 | "inputs": [ 206 | { 207 | "indexed": true, 208 | "internalType": "address", 209 | "name": "from", 210 | "type": "address" 211 | }, 212 | { 213 | "indexed": true, 214 | "internalType": "address", 215 | "name": "to", 216 | "type": "address" 217 | }, 218 | { 219 | "indexed": false, 220 | "internalType": "uint256", 221 | "name": "value", 222 | "type": "uint256" 223 | } 224 | ], 225 | "name": "Transfer", 226 | "type": "event" 227 | } 228 | ] 229 | -------------------------------------------------------------------------------- /abi/gas_price_oracle_abi.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "constant": true, 4 | "inputs": [], 5 | "name": "latestAnswer", 6 | "outputs": [ 7 | { 8 | "name": "", 9 | "type": "uint256" 10 | } 11 | ], 12 | "payable": false, 13 | "stateMutability": "view", 14 | "type": "function" 15 | } 16 | ] 17 | -------------------------------------------------------------------------------- /abi/uniswap_abi.json: -------------------------------------------------------------------------------- 1 | [{"inputs":[{"internalType":"address","name":"_factory","type":"address"},{"internalType":"address","name":"_WETH","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"WETH","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"tokenA","type":"address"},{"internalType":"address","name":"tokenB","type":"address"},{"internalType":"uint256","name":"amountADesired","type":"uint256"},{"internalType":"uint256","name":"amountBDesired","type":"uint256"},{"internalType":"uint256","name":"amountAMin","type":"uint256"},{"internalType":"uint256","name":"amountBMin","type":"uint256"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"addLiquidity","outputs":[{"internalType":"uint256","name":"amountA","type":"uint256"},{"internalType":"uint256","name":"amountB","type":"uint256"},{"internalType":"uint256","name":"liquidity","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"amountTokenDesired","type":"uint256"},{"internalType":"uint256","name":"amountTokenMin","type":"uint256"},{"internalType":"uint256","name":"amountETHMin","type":"uint256"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"addLiquidityETH","outputs":[{"internalType":"uint256","name":"amountToken","type":"uint256"},{"internalType":"uint256","name":"amountETH","type":"uint256"},{"internalType":"uint256","name":"liquidity","type":"uint256"}],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"factory","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"},{"internalType":"uint256","name":"reserveIn","type":"uint256"},{"internalType":"uint256","name":"reserveOut","type":"uint256"}],"name":"getAmountIn","outputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"uint256","name":"reserveIn","type":"uint256"},{"internalType":"uint256","name":"reserveOut","type":"uint256"}],"name":"getAmountOut","outputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"}],"name":"getAmountsIn","outputs":[{"internalType":"uint256[]","name":"amounts","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"}],"name":"getAmountsOut","outputs":[{"internalType":"uint256[]","name":"amounts","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountA","type":"uint256"},{"internalType":"uint256","name":"reserveA","type":"uint256"},{"internalType":"uint256","name":"reserveB","type":"uint256"}],"name":"quote","outputs":[{"internalType":"uint256","name":"amountB","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"tokenA","type":"address"},{"internalType":"address","name":"tokenB","type":"address"},{"internalType":"uint256","name":"liquidity","type":"uint256"},{"internalType":"uint256","name":"amountAMin","type":"uint256"},{"internalType":"uint256","name":"amountBMin","type":"uint256"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"removeLiquidity","outputs":[{"internalType":"uint256","name":"amountA","type":"uint256"},{"internalType":"uint256","name":"amountB","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"liquidity","type":"uint256"},{"internalType":"uint256","name":"amountTokenMin","type":"uint256"},{"internalType":"uint256","name":"amountETHMin","type":"uint256"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"removeLiquidityETH","outputs":[{"internalType":"uint256","name":"amountToken","type":"uint256"},{"internalType":"uint256","name":"amountETH","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"liquidity","type":"uint256"},{"internalType":"uint256","name":"amountTokenMin","type":"uint256"},{"internalType":"uint256","name":"amountETHMin","type":"uint256"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"removeLiquidityETHSupportingFeeOnTransferTokens","outputs":[{"internalType":"uint256","name":"amountETH","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"liquidity","type":"uint256"},{"internalType":"uint256","name":"amountTokenMin","type":"uint256"},{"internalType":"uint256","name":"amountETHMin","type":"uint256"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"bool","name":"approveMax","type":"bool"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"removeLiquidityETHWithPermit","outputs":[{"internalType":"uint256","name":"amountToken","type":"uint256"},{"internalType":"uint256","name":"amountETH","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"liquidity","type":"uint256"},{"internalType":"uint256","name":"amountTokenMin","type":"uint256"},{"internalType":"uint256","name":"amountETHMin","type":"uint256"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"bool","name":"approveMax","type":"bool"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"removeLiquidityETHWithPermitSupportingFeeOnTransferTokens","outputs":[{"internalType":"uint256","name":"amountETH","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"tokenA","type":"address"},{"internalType":"address","name":"tokenB","type":"address"},{"internalType":"uint256","name":"liquidity","type":"uint256"},{"internalType":"uint256","name":"amountAMin","type":"uint256"},{"internalType":"uint256","name":"amountBMin","type":"uint256"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"bool","name":"approveMax","type":"bool"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"removeLiquidityWithPermit","outputs":[{"internalType":"uint256","name":"amountA","type":"uint256"},{"internalType":"uint256","name":"amountB","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"swapETHForExactTokens","outputs":[{"internalType":"uint256[]","name":"amounts","type":"uint256[]"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountOutMin","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"swapExactETHForTokens","outputs":[{"internalType":"uint256[]","name":"amounts","type":"uint256[]"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountOutMin","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"swapExactETHForTokensSupportingFeeOnTransferTokens","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"uint256","name":"amountOutMin","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"swapExactTokensForETH","outputs":[{"internalType":"uint256[]","name":"amounts","type":"uint256[]"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"uint256","name":"amountOutMin","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"swapExactTokensForETHSupportingFeeOnTransferTokens","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"uint256","name":"amountOutMin","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"swapExactTokensForTokens","outputs":[{"internalType":"uint256[]","name":"amounts","type":"uint256[]"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"uint256","name":"amountOutMin","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"swapExactTokensForTokensSupportingFeeOnTransferTokens","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"},{"internalType":"uint256","name":"amountInMax","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"swapTokensForExactETH","outputs":[{"internalType":"uint256[]","name":"amounts","type":"uint256[]"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"},{"internalType":"uint256","name":"amountInMax","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"swapTokensForExactTokens","outputs":[{"internalType":"uint256[]","name":"amounts","type":"uint256[]"}],"stateMutability":"nonpayable","type":"function"},{"stateMutability":"payable","type":"receive"}] 2 | -------------------------------------------------------------------------------- /contracts/IERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0 2 | pragma solidity ^0.8.0; 3 | 4 | /** 5 | * @dev Interface of the ERC20 standard as defined in the EIP. 6 | */ 7 | interface IERC20 { 8 | /** 9 | * @dev Returns the amount of tokens in existence. 10 | */ 11 | function totalSupply() external view returns (uint256); 12 | 13 | /** 14 | * @dev Returns the amount of tokens owned by `account`. 15 | */ 16 | function balanceOf(address account) external view returns (uint256); 17 | 18 | /** 19 | * @dev Moves `amount` tokens from the caller's account to `recipient`. 20 | * 21 | * Returns a boolean value indicating whether the operation succeeded. 22 | * 23 | * Emits a {Transfer} event. 24 | */ 25 | function transfer(address recipient, uint256 amount) external returns (bool); 26 | 27 | /** 28 | * @dev Returns the remaining number of tokens that `spender` will be 29 | * allowed to spend on behalf of `owner` through {transferFrom}. This is 30 | * zero by default. 31 | * 32 | * This value changes when {approve} or {transferFrom} are called. 33 | */ 34 | function allowance(address owner, address spender) external view returns (uint256); 35 | 36 | /** 37 | * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. 38 | * 39 | * Returns a boolean value indicating whether the operation succeeded. 40 | * 41 | * IMPORTANT: Beware that changing an allowance with this method brings the risk 42 | * that someone may use both the old and the new allowance by unfortunate 43 | * transaction ordering. One possible solution to mitigate this race 44 | * condition is to first reduce the spender's allowance to 0 and set the 45 | * desired value afterwards: 46 | * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 47 | * 48 | * Emits an {Approval} event. 49 | */ 50 | function approve(address spender, uint256 amount) external returns (bool); 51 | 52 | /** 53 | * @dev Moves `amount` tokens from `sender` to `recipient` using the 54 | * allowance mechanism. `amount` is then deducted from the caller's 55 | * allowance. 56 | * 57 | * Returns a boolean value indicating whether the operation succeeded. 58 | * 59 | * Emits a {Transfer} event. 60 | */ 61 | function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); 62 | 63 | /** 64 | * @dev Emitted when `value` tokens are moved from one account (`from`) to 65 | * another (`to`). 66 | * 67 | * Note that `value` may be zero. 68 | */ 69 | event Transfer(address indexed from, address indexed to, uint256 value); 70 | 71 | /** 72 | * @dev Emitted when the allowance of a `spender` for an `owner` is set by 73 | * a call to {approve}. `value` is the new allowance. 74 | */ 75 | event Approval(address indexed owner, address indexed spender, uint256 value); 76 | } -------------------------------------------------------------------------------- /contracts/SimpleFlashloan.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.20; 2 | 3 | import "https://github.com/aave/aave-v3-core/blob/master/contracts/flashloan/base/FlashLoanSimpleReceiverBase.sol"; 4 | import "https://github.com/aave/aave-v3-core/blob/master/contracts/interfaces/IPoolAddressesProvider.sol"; 5 | import "https://github.com/aave/aave-v3-core/blob/master/contracts/dependencies/openzeppelin/contracts/IERC20.sol"; 6 | 7 | interface IGasPriceOracle { 8 | function latestAnswer() external view returns (int256); 9 | } 10 | 11 | contract SimpleFlashLoan is FlashLoanSimpleReceiverBase { 12 | address payable public owner; 13 | IGasPriceOracle public gasPriceOracle; 14 | bool private locked; 15 | 16 | event FlashLoanRequested(address token, uint256 amount); 17 | event FlashLoanExecuted(address token, uint256 amount, uint256 premium, bool success); 18 | event FlashLoanFailed(address token, uint256 amount, uint256 premium, string reason); 19 | 20 | constructor(address _addressProvider, address _gasPriceOracle) FlashLoanSimpleReceiverBase(IPoolAddressesProvider(_addressProvider)) { 21 | owner = payable(msg.sender); // Contract deployer becomes the owner 22 | gasPriceOracle = IGasPriceOracle(_gasPriceOracle); 23 | locked = false; 24 | } 25 | 26 | modifier onlyOwner() { 27 | require(msg.sender == owner, "Not contract owner"); 28 | _; 29 | } 30 | 31 | modifier nonReentrant() { 32 | require(!locked, "Reentrant call"); 33 | locked = true; 34 | _; 35 | locked = false; 36 | } 37 | 38 | function fn_RequestFlashLoan(address[] memory _tokens, uint256[] memory _amounts) public { 39 | require(_tokens.length == _amounts.length, "Tokens and amounts length mismatch"); 40 | for (uint256 i = 0; i < _tokens.length; i++) { 41 | address receiverAddress = address(this); 42 | address asset = _tokens[i]; 43 | uint256 amount = _amounts[i]; 44 | bytes memory params = ""; 45 | uint16 referralCode = 0; 46 | 47 | emit FlashLoanRequested(_tokens[i], _amounts[i]); 48 | 49 | POOL.flashLoanSimple( 50 | receiverAddress, 51 | asset, 52 | amount, 53 | params, 54 | referralCode 55 | ); 56 | } 57 | } 58 | 59 | function executeOperation( 60 | address asset, 61 | uint256 amount, 62 | uint256 premium, 63 | address initiator, 64 | bytes calldata params 65 | ) external override nonReentrant returns (bool) { 66 | try { 67 | // Add your flash loan logic here 68 | 69 | emit FlashLoanExecuted(asset, amount, premium, true); 70 | return true; 71 | } catch (bytes memory reason) { 72 | emit FlashLoanFailed(asset, amount, premium, string(reason)); 73 | return false; 74 | } 75 | } 76 | 77 | function withdrawToken(address _tokenAddress) public onlyOwner { 78 | IERC20 token = IERC20(_tokenAddress); 79 | uint256 balance = token.balanceOf(address(this)); 80 | require(balance > 0, "No tokens to withdraw"); 81 | token.transfer(owner, balance); 82 | } 83 | 84 | function withdrawETH() public onlyOwner { 85 | uint256 balance = address(this).balance; 86 | require(balance > 0, "No ETH to withdraw"); 87 | owner.transfer(balance); 88 | } 89 | 90 | receive() external payable {} 91 | } 92 | -------------------------------------------------------------------------------- /linear_regression/price_model.joblib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/John0n1/ON1Builder/a6d76156d6f379560c3db859f01573bca39e10fd/linear_regression/price_model.joblib -------------------------------------------------------------------------------- /linear_regression/training_data.csv: -------------------------------------------------------------------------------- 1 | # ./ON1Builder/linear_regression/training_data.csv,timestamp,symbol,price_usd,market_cap,volume_24h,percent_change_24h,total_supply,circulating_supply,volatility,liquidity_ratio,avg_transaction_value,trading_pairs,exchange_count,price_momentum,buy_sell_ratio,smart_money_flow 2 | ,1745095776.0,WETH,1.0,195285181779.0,5532258739.62458,0.0,120703159.4902972,120703159.4902972,0.0,0.0283291271218177,0.0,100.0,0.0,0.0,1.0,0.0 3 | ,1745096597.0,WETH,1.0,195319683591.0,5611138866.311223,0.0,120703159.4902972,120703159.4902972,0.0,0.0287279743810202,0.0,100.0,0.0,0.0,1.0,0.0 4 | ,1745096597.0,ETH,1.0,195319683591.0,5611138866.311223,0.0,120703159.4902972,120703159.4902972,0.0,0.0287279743810202,0.0,100.0,0.0,0.0,1.0,0.0 5 | ,1745096881.0,WETH,1.0,195425037159.0,5638254506.901199,0.0,120703159.4902972,120703159.4902972,0.0,0.0288512392724472,0.0,100.0,0.0,0.0,1.0,0.0 6 | ,1745096881.0,ETH,1.0,195425037159.0,5638254506.901199,0.0,120703159.4902972,120703159.4902972,0.0,0.0288512392724472,0.0,100.0,0.0,0.0,1.0,0.0 7 | ,1745099126.0,WETH,1.0,194949393381.0,4989132649.176152,0.0,120703159.4902972,120703159.4902972,0.0,0.0255919372851067,0.0,100.0,0.0,0.0,1.0,0.0 8 | ,1745099126.0,ETH,1.0,194949393381.0,4989132649.176152,0.0,120703159.4902972,120703159.4902972,0.0,0.0255919372851067,0.0,100.0,0.0,0.0,1.0,0.0 9 | ,1745099692.0,WETH,1.0,194928182376.0,5134772890.430519,0.0,120703159.4902972,120703159.4902972,0.0,0.026341870261355926,0.0,100.0,0.0,0.0,1.0,0.0 10 | ,1745099692.0,ETH,1.0,194928182376.0,5134772890.430519,0.0,120703159.4902972,120703159.4902972,0.0,0.026341870261355926,0.0,100.0,0.0,0.0,1.0,0.0 11 | smart_money_flow,,,,,,,,,,,,,,,, 12 | -------------------------------------------------------------------------------- /python/__init__.py: -------------------------------------------------------------------------------- 1 | # LICENSE: MIT // github.com/John0n1/ON1Builder 2 | 3 | # This file is part of the ON1Builder package. 4 | 5 | """ 6 | ON1Builder package initialization. 7 | """ 8 | -------------------------------------------------------------------------------- /python/abiregistry.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import aiofiles 3 | import json 4 | from pathlib import Path 5 | from typing import Any, Dict, List, Optional 6 | 7 | from eth_utils import function_signature_to_4byte_selector 8 | from loggingconfig import setup_logging 9 | import logging 10 | 11 | logger = setup_logging("AbiRegistry", level=logging.DEBUG) 12 | 13 | class ABIRegistry: 14 | """ 15 | Centralized ABI registry that loads, validates, and extracts function signatures. 16 | 17 | Attributes: 18 | REQUIRED_METHODS (dict): Mapping of ABI types to sets of required method names. 19 | abis (dict): Dictionary of loaded ABIs by type. 20 | signatures (dict): For each ABI type, a mapping from method names to full signature strings. 21 | method_selectors (dict): For each ABI type, a mapping from 4-byte hex selectors to method names. 22 | """ 23 | REQUIRED_METHODS = { 24 | 'erc20': {'transfer', 'approve', 'transferFrom', 'balanceOf'}, 25 | 'uniswap': {'swapExactTokensForTokens', 'swapTokensForExactTokens', 'addLiquidity', 'getAmountsOut'}, 26 | 'sushiswap': {'swapExactTokensForTokens', 'swapTokensForExactTokens', 'addLiquidity', 'getAmountsOut'}, 27 | 'aave_flashloan': {'fn_RequestFlashLoan', 'executeOperation', 'ADDRESSES_PROVIDER', 'POOL'}, 28 | 'aave': {'admin', 'implementation', 'upgradeTo', 'upgradeToAndCall'} 29 | } 30 | 31 | def __init__(self) -> None: 32 | self.abis: Dict[str, List[Dict[str, Any]]] = {} 33 | self.signatures: Dict[str, Dict[str, str]] = {} 34 | self.method_selectors: Dict[str, Dict[str, str]] = {} 35 | self._initialized: bool = False 36 | 37 | async def initialize(self, base_path: Optional[Path] = None) -> None: 38 | """ 39 | Asynchronously load and validate all ABIs from the 'abi' directory. 40 | 41 | Args: 42 | base_path (Optional[Path]): Base path where the ABI folder is located. 43 | Defaults to the parent of the parent directory. 44 | """ 45 | if self._initialized: 46 | logger.debug("ABIRegistry already initialized.") 47 | return 48 | 49 | await self._load_all_abis(base_path) 50 | self._initialized = True 51 | logger.debug("ABIRegistry initialization complete.") 52 | 53 | async def _load_all_abis(self, base_path: Optional[Path] = None) -> None: 54 | """ 55 | Load all ABIs from the expected files in the 'abi' directory. 56 | """ 57 | if not base_path: 58 | base_path = Path(__file__).parent.parent 59 | abi_dir = base_path / "abi" 60 | logger.debug(f"Loading ABIs from directory: {abi_dir}") 61 | 62 | # Define expected ABI files by type. 63 | abi_files = { 64 | "erc20": "erc20_abi.json", 65 | "uniswap": "uniswap_abi.json", 66 | "sushiswap": "sushiswap_abi.json", 67 | "aave_flashloan": "aave_flashloan_abi.json", 68 | "aave": "aave_pool_abi.json" 69 | } 70 | # Critical ABIs that must be loaded (if one fails, we want to stop). 71 | critical_abis = {"erc20", "uniswap"} 72 | 73 | tasks = [] 74 | for abi_type, filename in abi_files.items(): 75 | filepath = abi_dir / filename 76 | tasks.append(self._load_and_validate_abi(abi_type, filepath, critical_abis)) 77 | await asyncio.gather(*tasks) 78 | 79 | async def _load_and_validate_abi(self, abi_type: str, abi_path: Path, critical_abis: set) -> None: 80 | """ 81 | Load a single ABI file and validate its contents. 82 | """ 83 | try: 84 | abi = await self._load_abi_from_path(abi_path, abi_type) 85 | self.abis[abi_type] = abi 86 | self._extract_signatures(abi, abi_type) 87 | logger.info(f"Loaded and validated {abi_type} ABI from {abi_path}") 88 | except Exception as e: 89 | logger.error(f"Error loading {abi_type} ABI from {abi_path}: {e}") 90 | if abi_type in critical_abis: 91 | raise 92 | else: 93 | logger.warning(f"Skipping non-critical ABI: {abi_type}") 94 | 95 | async def _load_abi_from_path(self, abi_path: Path, abi_type: str) -> List[Dict[str, Any]]: 96 | """ 97 | Asynchronously load an ABI from a JSON file. 98 | 99 | Args: 100 | abi_path (Path): The path to the ABI file. 101 | abi_type (str): The ABI type. 102 | 103 | Returns: 104 | List[Dict[str, Any]]: The loaded ABI. 105 | 106 | Raises: 107 | FileNotFoundError: If the file does not exist. 108 | ValueError: If JSON decoding or validation fails. 109 | """ 110 | if not abi_path.exists(): 111 | msg = f"ABI file for {abi_type} not found at {abi_path}" 112 | logger.error(msg) 113 | raise FileNotFoundError(msg) 114 | 115 | try: 116 | async with aiofiles.open(abi_path, "r", encoding="utf-8") as f: 117 | content = await f.read() 118 | abi = json.loads(content) 119 | except json.JSONDecodeError as e: 120 | raise ValueError(f"Invalid JSON in {abi_path}: {e}") 121 | except Exception as e: 122 | raise RuntimeError(f"Error reading ABI from {abi_path}: {e}") 123 | 124 | if not self._validate_abi(abi, abi_type): 125 | raise ValueError(f"ABI validation failed for {abi_type} at {abi_path}") 126 | return abi 127 | 128 | def _validate_abi(self, abi: List[Dict[str, Any]], abi_type: str) -> bool: 129 | """ 130 | Validate that the ABI is a list and contains required methods. 131 | 132 | Args: 133 | abi (List[Dict[str, Any]]): The ABI to validate. 134 | abi_type (str): The type/category of the ABI. 135 | 136 | Returns: 137 | bool: True if valid, else False. 138 | """ 139 | if not isinstance(abi, list): 140 | logger.error(f"ABI for {abi_type} is not a list.") 141 | return False 142 | 143 | found_methods = {entry.get("name") for entry in abi if entry.get("type") == "function" and entry.get("name")} 144 | required = self.REQUIRED_METHODS.get(abi_type, set()) 145 | missing = required - found_methods 146 | if missing: 147 | logger.error(f"ABI for {abi_type} is missing required methods: {missing}") 148 | return False 149 | return True 150 | 151 | def _extract_signatures(self, abi: List[Dict[str, Any]], abi_type: str) -> None: 152 | """ 153 | Extract full function signatures and their 4-byte selectors from the ABI. 154 | 155 | Args: 156 | abi (List[Dict[str, Any]]): The ABI. 157 | abi_type (str): The ABI type. 158 | """ 159 | sigs = {} 160 | selectors = {} 161 | 162 | for entry in abi: 163 | if entry.get("type") == "function" and entry.get("name"): 164 | func_name = entry["name"] 165 | inputs = entry.get("inputs", []) 166 | types = ",".join(inp.get("type", "") for inp in inputs) 167 | signature = f"{func_name}({types})" 168 | selector = function_signature_to_4byte_selector(signature).hex() 169 | sigs[func_name] = signature 170 | selectors[selector] = func_name 171 | 172 | self.signatures[abi_type] = sigs 173 | self.method_selectors[abi_type] = selectors 174 | 175 | def get_abi(self, abi_type: str) -> Optional[List[Dict[str, Any]]]: 176 | """ 177 | Retrieve the ABI for a given type. 178 | """ 179 | return self.abis.get(abi_type) 180 | 181 | def get_method_selector(self, selector: str) -> Optional[str]: 182 | """ 183 | Get the method name corresponding to a 4-byte selector across all ABIs. 184 | """ 185 | for selectors in self.method_selectors.values(): 186 | if selector in selectors: 187 | return selectors[selector] 188 | return None 189 | 190 | def get_function_signature(self, abi_type: str, method_name: str) -> Optional[str]: 191 | """ 192 | Retrieve the full function signature for a given ABI type and method. 193 | """ 194 | return self.signatures.get(abi_type, {}).get(method_name) 195 | 196 | async def update_abi(self, abi_type: str, new_abi: List[Dict[str, Any]]) -> None: 197 | """ 198 | Update an ABI with new data dynamically. 199 | 200 | Args: 201 | abi_type (str): The type to update. 202 | new_abi (List[Dict[str, Any]]): The new ABI. 203 | 204 | Raises: 205 | ValueError: If the new ABI fails validation. 206 | """ 207 | if not self._validate_abi(new_abi, abi_type): 208 | raise ValueError(f"Validation failed for {abi_type} ABI update.") 209 | self.abis[abi_type] = new_abi 210 | self._extract_signatures(new_abi, abi_type) 211 | logger.info(f"Updated {abi_type} ABI dynamically.") 212 | 213 | async def validate_abi(self, abi_type: str) -> bool: 214 | """ 215 | Validate the ABI for a given type. 216 | 217 | Args: 218 | abi_type (str): The type of the ABI to validate. 219 | 220 | Returns: 221 | bool: True if the ABI is valid, else False. 222 | """ 223 | abi = self.abis.get(abi_type) 224 | if not abi: 225 | logger.error(f"ABI for {abi_type} not found.") 226 | return False 227 | return self._validate_abi(abi, abi_type) 228 | -------------------------------------------------------------------------------- /python/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, jsonify, request, send_from_directory 2 | from flask_cors import CORS 3 | from flask_socketio import SocketIO, emit 4 | import threading 5 | import asyncio 6 | import time 7 | import queue 8 | import logging 9 | 10 | import sys 11 | import os 12 | sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'python'))) 13 | from maincore import MainCore 14 | from configuration import Configuration 15 | from loggingconfig import setup_logging 16 | 17 | app = Flask(__name__) 18 | CORS(app) 19 | socketio = SocketIO(app, cors_allowed_origins="*") 20 | 21 | logger = setup_logging("FlaskUI", level=20) 22 | 23 | bot_running = False 24 | bot_thread = None 25 | bot_loop = None 26 | main_core = None 27 | main_core_lock = threading.Lock() 28 | 29 | class WebSocketLogHandler(logging.Handler): 30 | def __init__(self): 31 | super().__init__() 32 | self.log_queue = queue.Queue() 33 | self.current_level = logging.INFO 34 | 35 | def emit(self, record): 36 | try: 37 | log_entry = { 38 | 'level': record.levelname, 39 | 'message': self.format(record), 40 | 'timestamp': time.strftime('%H:%M:%S', time.localtime(record.created)) 41 | } 42 | self.log_queue.put(log_entry) 43 | socketio.emit('log_message', log_entry) 44 | except Exception: 45 | self.handleError(record) 46 | 47 | # Initialize WebSocket logging 48 | ws_handler = WebSocketLogHandler() 49 | ws_handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')) 50 | logging.getLogger().addHandler(ws_handler) 51 | 52 | def run_bot_in_thread(): 53 | global bot_running, bot_loop, main_core 54 | asyncio.set_event_loop(asyncio.new_event_loop()) 55 | bot_loop = asyncio.get_event_loop() 56 | configuration = Configuration() 57 | main_core = MainCore(configuration) 58 | try: 59 | bot_loop.run_until_complete(main_core.initialize_components()) 60 | bot_loop.run_until_complete(main_core.run()) 61 | except Exception as e: 62 | logger.error(f"Bot error: {e}") 63 | finally: 64 | bot_running = False 65 | 66 | @app.route('/') 67 | def index(): 68 | # Serve the UI index.html file 69 | ui_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'ui')) 70 | return send_from_directory(ui_dir, 'index.html') 71 | 72 | @app.route('/start', methods=['POST']) 73 | def start_bot(): 74 | global bot_running, bot_thread 75 | if not bot_running: 76 | bot_running = True 77 | bot_thread = threading.Thread(target=run_bot_in_thread, daemon=True) 78 | bot_thread.start() 79 | return jsonify({"status": "Bot started"}), 200 80 | else: 81 | return jsonify({"status": "Bot is already running"}), 400 82 | 83 | @app.route('/stop', methods=['POST']) 84 | def stop_bot(): 85 | global bot_running, main_core, bot_loop 86 | if bot_running: 87 | bot_running = False 88 | if main_core and bot_loop: 89 | try: 90 | bot_loop.call_soon_threadsafe(asyncio.create_task, main_core.stop()) 91 | except Exception as e: 92 | logger.error(f"Error stopping bot: {e}") 93 | return jsonify({"status": "Bot stopped"}), 200 94 | else: 95 | return jsonify({"status": "Bot is not running"}), 400 96 | 97 | @app.route('/status', methods=['GET']) 98 | def get_status(): 99 | global bot_running, main_core 100 | status = {"running": bot_running} 101 | if main_core: 102 | status["components"] = {k: v is not None for k, v in getattr(main_core, "components", {}).items()} 103 | return jsonify(status), 200 104 | 105 | def get_metrics_sync(coro): 106 | global bot_loop 107 | if bot_loop and bot_loop.is_running(): 108 | fut = asyncio.run_coroutine_threadsafe(coro, bot_loop) 109 | try: 110 | return fut.result(timeout=5) 111 | except Exception as e: 112 | logger.error(f"Error fetching metrics: {e}") 113 | return None 114 | return None 115 | 116 | def get_real_metrics(): 117 | global main_core 118 | default_metrics = { 119 | "transaction_success_rate": 0.0, 120 | "average_execution_time": 0.0, 121 | "profitability": 0.0, 122 | "gas_usage": 0, 123 | "network_congestion": 0.0, 124 | "slippage": 0.0, 125 | "balance": 0.0, 126 | "number_of_transactions": 0 127 | } 128 | if not main_core or not hasattr(main_core, "components"): 129 | return default_metrics 130 | try: 131 | comps = main_core.components 132 | tc = comps.get("transactioncore") 133 | sn = comps.get("safetynet") 134 | stn = comps.get("strategynet") 135 | # Fetch balance asynchronously 136 | balance = 0.0 137 | if sn and hasattr(sn, "account"): 138 | balance = float(get_metrics_sync(sn.get_balance(sn.account)) or 0.0) 139 | # Fetch network congestion asynchronously 140 | network_congestion = 0.0 141 | if sn: 142 | network_congestion = float(get_metrics_sync(sn.get_network_congestion()) or 0.0) 143 | # Strategy performance 144 | strategy_perf = None 145 | if stn and hasattr(stn, "strategy_performance"): 146 | perf = stn.strategy_performance.get("front_run") 147 | if perf: 148 | strategy_perf = perf 149 | metrics = { 150 | "transaction_success_rate": getattr(strategy_perf, "success_rate", 0.0) if strategy_perf else 0.0, 151 | "average_execution_time": getattr(strategy_perf, "avg_execution_time", 0.0) if strategy_perf else 0.0, 152 | "profitability": float(getattr(strategy_perf, "profit", 0.0)) if strategy_perf else 0.0, 153 | "gas_usage": getattr(tc, "DEFAULT_GAS_LIMIT", 0) if tc else 0, 154 | "network_congestion": network_congestion, 155 | "slippage": sn.SLIPPAGE_CONFIG["default"] if sn and hasattr(sn, "SLIPPAGE_CONFIG") else 0.0, 156 | "balance": balance, 157 | "number_of_transactions": getattr(strategy_perf, "total_executions", 0) if strategy_perf else 0 158 | } 159 | return metrics 160 | except Exception as e: 161 | logger.error(f"Error fetching real metrics: {e}") 162 | return default_metrics 163 | 164 | @app.route('/metrics', methods=['GET']) 165 | def get_metrics(): 166 | metrics = get_real_metrics() 167 | return jsonify(metrics), 200 168 | 169 | @app.route('/components', methods=['GET']) 170 | def get_components(): 171 | global main_core 172 | if not main_core or not hasattr(main_core, "components"): 173 | return jsonify({"error": "Bot not running"}), 400 174 | status = {k: v is not None for k, v in main_core.components.items()} 175 | return jsonify(status), 200 176 | 177 | @app.route('/set_log_level', methods=['POST']) 178 | def set_log_level(): 179 | level = request.json.get('level', 'INFO') 180 | level_map = { 181 | 'DEBUG': logging.DEBUG, 182 | 'INFO': logging.INFO 183 | } 184 | if level in level_map: 185 | ws_handler.current_level = level_map[level] 186 | logging.getLogger().setLevel(level_map[level]) 187 | return jsonify({"status": "Log level updated", "level": level}), 200 188 | return jsonify({"error": "Invalid log level"}), 400 189 | 190 | @socketio.on('connect') 191 | def handle_connect(): 192 | recent_logs = list(ws_handler.log_queue.queue)[-100:] # Get last 100 logs 193 | emit('initial_logs', recent_logs) 194 | 195 | if __name__ == '__main__': 196 | socketio.run(app, host='0.0.0.0', port=5000, debug=True) 197 | -------------------------------------------------------------------------------- /python/loggingconfig.py: -------------------------------------------------------------------------------- 1 | # LICENSE: MIT // github.com/John0n1/ON1Builder 2 | 3 | import logging 4 | import sys 5 | import threading 6 | import time 7 | import colorlog 8 | 9 | def setup_logging(name: str, level: int = logging.INFO, spinner: bool = False, spinner_message: str = "Loading") -> logging.Logger: 10 | """ 11 | Set up logging for the application with optional spinner animation. 12 | 13 | Args: 14 | name (str): Name of the logger. 15 | level (int): Logging level (default: logging.INFO). 16 | spinner (bool): Whether to show a spinner animation (default: False). 17 | spinner_message (str): Message to display with the spinner (default: "Loading"). 18 | 19 | Returns: 20 | logging.Logger: Configured logger instance. 21 | """ 22 | def spinner_task(message: str, stop_event: threading.Event) -> None: 23 | """ 24 | Spinner task for smoother animation. 25 | 26 | Args: 27 | message (str): Message to display with the spinner. 28 | stop_event (threading.Event): Event to stop the spinner. 29 | """ 30 | spinner_chars = ['|', '/', '-', '\\'] 31 | idx = 0 32 | while not stop_event.is_set(): 33 | sys.stdout.write(f"\r{message}... {spinner_chars[idx % len(spinner_chars)]}") 34 | sys.stdout.flush() 35 | time.sleep(0.1) 36 | idx += 1 37 | sys.stdout.write("\r" + " " * (len(message) + 10) + "\r") 38 | sys.stdout.flush() 39 | 40 | if spinner: 41 | stop_event = threading.Event() 42 | thread = threading.Thread(target=spinner_task, args=(spinner_message, stop_event), daemon=True) 43 | thread.start() 44 | 45 | logger = logging.getLogger(name) 46 | logger.setLevel(level) 47 | if logger.hasHandlers(): 48 | logger.handlers.clear() 49 | 50 | handler = colorlog.StreamHandler(sys.stdout) 51 | formatter = colorlog.ColoredFormatter( 52 | '%(log_color)s%(name)s | %(levelname)-8s: %(message)s', 53 | log_colors={ 54 | 'DEBUG': 'cyan', 55 | 'INFO': 'green', 56 | 'WARNING': 'yellow', 57 | 'ERROR': 'red', 58 | 'CRITICAL': 'bold_red' 59 | } 60 | ) 61 | handler.setFormatter(formatter) 62 | logger.addHandler(handler) 63 | 64 | if spinner: 65 | stop_event.set() 66 | thread.join() 67 | 68 | return logger 69 | # --- End file: loggingconfig.py --- 70 | -------------------------------------------------------------------------------- /python/main.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from maincore import MainCore 3 | from configuration import Configuration 4 | from loggingconfig import setup_logging 5 | import logging 6 | 7 | logger = setup_logging("Main", level=logging.DEBUG) 8 | 9 | async def run_bot() -> None: 10 | configuration = Configuration() 11 | core = MainCore(configuration) 12 | await core.initialize_components() 13 | await core.run() 14 | 15 | async def main() -> None: 16 | await run_bot() 17 | 18 | if __name__ == "__main__": 19 | try: 20 | asyncio.run(main()) 21 | except KeyboardInterrupt: 22 | pass 23 | except Exception as e: 24 | logger.critical(f"Fatal error in main: {e}") 25 | -------------------------------------------------------------------------------- /python/maincore.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import tracemalloc 3 | import async_timeout 4 | import time 5 | import os 6 | import sys 7 | import signal 8 | from typing import Any, Dict, List, Optional 9 | 10 | from web3 import AsyncWeb3 11 | from web3.eth import AsyncEth 12 | from web3.middleware import ExtraDataToPOAMiddleware 13 | from web3 import AsyncIPCProvider, AsyncHTTPProvider, WebSocketProvider 14 | from eth_account import Account 15 | 16 | from abiregistry import ABIRegistry 17 | from apiconfig import APIConfig 18 | from configuration import Configuration 19 | from marketmonitor import MarketMonitor 20 | from mempoolmonitor import MempoolMonitor 21 | from noncecore import NonceCore 22 | from safetynet import SafetyNet 23 | from strategynet import StrategyNet 24 | from transactioncore import TransactionCore 25 | 26 | from loggingconfig import setup_logging 27 | import logging 28 | 29 | logger = setup_logging("MainCore", level=logging.DEBUG) 30 | 31 | class MainCore: 32 | """ 33 | Orchestrates all components (configuration, blockchain connection, account, 34 | noncecore, safetynet, transactioncore, marketmonitor, mempoolmonitor, strategynet) 35 | and manages main loop, memory monitoring, and graceful shutdown. 36 | """ 37 | def __init__(self, configuration: Configuration) -> None: 38 | tracemalloc.start() 39 | self.memory_snapshot = tracemalloc.take_snapshot() 40 | self.configuration = configuration 41 | self.web3: Optional[AsyncWeb3] = None 42 | self.account: Optional[Account] = None 43 | self.running: bool = False 44 | self._shutdown_event: asyncio.Event = asyncio.Event() 45 | self.components: Dict[str, Optional[Any]] = { 46 | "apiconfig": None, 47 | "noncecore": None, 48 | "safetynet": None, 49 | "marketmonitor": None, 50 | "mempoolmonitor": None, 51 | "transactioncore": None, 52 | "strategynet": None, 53 | } 54 | self._component_health: Dict[str, bool] = {name: False for name in self.components} 55 | self.WEB3_MAX_RETRIES = self.configuration.get_config_value("WEB3_MAX_RETRIES", 3) 56 | logger.info("Initializing ON1Builder...") 57 | self.loop = asyncio.get_event_loop() 58 | 59 | async def _load_configuration(self) -> None: 60 | try: 61 | await self.configuration.load() 62 | logger.info("Configuration successfully loaded.") 63 | except Exception as e: 64 | logger.critical(f"Configuration load failed: {e}", exc_info=True) 65 | raise 66 | 67 | async def _initialize_web3(self) -> Optional[AsyncWeb3]: 68 | providers = await self._get_providers() 69 | if not providers: 70 | logger.error("No valid Web3 provider endpoints available!") 71 | return None 72 | max_retries = self.configuration.get_config_value("WEB3_MAX_RETRIES", 3) 73 | retry_delay = self.configuration.get_config_value("WEB3_RETRY_DELAY", 2) 74 | for provider_name, provider in providers: 75 | for attempt in range(max_retries): 76 | try: 77 | logger.debug(f"Connecting via {provider_name}, attempt {attempt+1}.") 78 | web3 = AsyncWeb3(provider, modules={"eth": (AsyncEth,)}) 79 | async with async_timeout.timeout(10): 80 | if await web3.is_connected(): 81 | chain_id = await web3.eth.chain_id 82 | logger.info(f"Connected via {provider_name} (Chain ID: {chain_id}).") 83 | await self._add_middleware(web3) 84 | return web3 85 | except Exception as e: 86 | logger.warning(f"{provider_name} connection attempt {attempt+1} failed: {e}") 87 | await asyncio.sleep(retry_delay * (attempt+1)) 88 | logger.error(f"All attempts failed for {provider_name}.") 89 | return None 90 | 91 | async def _get_providers(self) -> List[tuple]: 92 | providers = [] 93 | if self.configuration.HTTP_ENDPOINT: 94 | http_provider = AsyncHTTPProvider(self.configuration.HTTP_ENDPOINT) 95 | try: 96 | await http_provider.make_request("eth_blockNumber", []) 97 | providers.append(("HTTP Provider", http_provider)) 98 | logger.info("Connected via HTTP Provider.") 99 | return providers 100 | except Exception as e: 101 | logger.warning(f"HTTP Provider error: {e}") 102 | if self.configuration.WEBSOCKET_ENDPOINT: 103 | try: 104 | ws_provider = WebSocketProvider(self.configuration.WEBSOCKET_ENDPOINT) 105 | await ws_provider.connect() 106 | providers.append(("WebSocket Provider", ws_provider)) 107 | logger.info("Connected via WebSocket Provider.") 108 | return providers 109 | except Exception as e: 110 | logger.warning(f"WebSocket Provider error: {e}; trying IPC.") 111 | if self.configuration.IPC_ENDPOINT: 112 | try: 113 | ipc_provider = AsyncIPCProvider(self.configuration.IPC_ENDPOINT) 114 | await ipc_provider.make_request("eth_blockNumber", []) 115 | providers.append(("IPC Provider", ipc_provider)) 116 | logger.info("Connected via IPC Provider.") 117 | return providers 118 | except Exception as e: 119 | logger.warning(f"IPC Provider error: {e}") 120 | logger.critical("No provider available.") 121 | return providers 122 | 123 | async def _add_middleware(self, web3: AsyncWeb3) -> None: 124 | try: 125 | chain_id = await web3.eth.chain_id 126 | if chain_id in {99, 100, 77, 7766, 56}: 127 | web3.middleware_onion.inject(ExtraDataToPOAMiddleware, layer=0) 128 | logger.info("Injected POA middleware.") 129 | else: 130 | logger.info("No middleware needed for this network.") 131 | except Exception as e: 132 | logger.error(f"Middleware error: {e}", exc_info=True) 133 | raise 134 | 135 | async def _check_account_balance(self) -> None: 136 | if not self.account or not self.web3: 137 | raise ValueError("Account or Web3 not initialized.") 138 | try: 139 | min_balance = float(self.configuration.get_config_value("MIN_BALANCE", 0.000001)) 140 | balance = await self.web3.eth.get_balance(self.account.address) 141 | balance_eth = float(self.web3.from_wei(balance, "ether")) 142 | logger.info(f"Account {self.account.address[:8]}... Balance: {balance_eth:.4f} ETH") 143 | if balance_eth < min_balance: 144 | logger.warning(f"Low balance: {balance_eth:.4f} ETH below threshold {min_balance} ETH") 145 | except Exception as e: 146 | logger.error(f"Error checking account balance: {e}", exc_info=True) 147 | raise 148 | 149 | async def initialize_components(self) -> None: 150 | """ 151 | Initialize all components in order: configuration, web3, account, ABI registry, 152 | API configuration, noncecore, safetynet, transactioncore, marketmonitor, mempoolmonitor, strategynet. 153 | """ 154 | try: 155 | await self._load_configuration() 156 | self.web3 = await self._initialize_web3() 157 | if not self.web3: 158 | raise RuntimeError("Web3 initialization failed.") 159 | self.account = Account.from_key(self.configuration.WALLET_KEY) 160 | await self._check_account_balance() 161 | 162 | # Initialize API Configuration 163 | self.components["apiconfig"] = APIConfig(self.configuration) 164 | await self.components["apiconfig"].initialize() 165 | 166 | # Initialize NonceCore 167 | self.components["noncecore"] = NonceCore(self.web3, self.account.address, self.configuration) 168 | await self.components["noncecore"].initialize() 169 | 170 | # Initialize SafetyNet 171 | self.components["safetynet"] = SafetyNet(self.web3, self.configuration, self.account.address, self.account, self.components["apiconfig"], self.components.get("marketmonitor")) 172 | await self.components["safetynet"].initialize() 173 | 174 | # Initialize MarketMonitor 175 | self.components["marketmonitor"] = MarketMonitor( 176 | web3=self.web3, 177 | configuration=self.configuration, 178 | apiconfig=self.components["apiconfig"], 179 | transactioncore=self.components["transactioncore"] 180 | ) 181 | await self.components["marketmonitor"].initialize() 182 | 183 | # Initialize TransactionCore 184 | self.components["transactioncore"] = TransactionCore( 185 | self.web3, 186 | self.account, 187 | self.configuration, 188 | noncecore=self.components["noncecore"], 189 | safetynet=self.components["safetynet"], 190 | apiconfig=self.components["apiconfig"], 191 | marketmonitor=self.components["marketmonitor"] 192 | ) 193 | await self.components["transactioncore"].initialize() 194 | 195 | # Initialize MempoolMonitor 196 | token_addresses = await self.configuration.get_token_addresses() 197 | self.components["mempoolmonitor"] = MempoolMonitor( 198 | web3=self.web3, 199 | safetynet=self.components["safetynet"], 200 | noncecore=self.components["noncecore"], 201 | apiconfig=self.components["apiconfig"], 202 | monitored_tokens=token_addresses, 203 | configuration=self.configuration, 204 | marketmonitor=self.components["marketmonitor"] 205 | ) 206 | await self.components["mempoolmonitor"].initialize() 207 | 208 | # Initialize StrategyNet 209 | self.components["strategynet"] = StrategyNet( 210 | self.components["transactioncore"], 211 | self.components["marketmonitor"], 212 | self.components["safetynet"], 213 | self.components["apiconfig"] 214 | ) 215 | await self.components["strategynet"].initialize() 216 | 217 | logger.info("All components initialized successfully.") 218 | except Exception as e: 219 | logger.error(f"Component initialization failed: {e}", exc_info=True) 220 | raise 221 | 222 | async def _check_component_health(self) -> None: 223 | while self.running: 224 | try: 225 | for name, component in self.components.items(): 226 | if component and hasattr(component, "is_healthy"): 227 | self._component_health[name] = await component.is_healthy() 228 | else: 229 | self._component_health[name] = component is not None 230 | if not all(self._component_health.values()): 231 | unhealthy = [name for name, healthy in self._component_health.items() if not healthy] 232 | logger.warning(f"Unhealthy components: {unhealthy}") 233 | await asyncio.sleep(self.configuration.COMPONENT_HEALTH_CHECK_INTERVAL) 234 | except asyncio.CancelledError: 235 | logger.info("Component health check cancelled.") 236 | break 237 | except Exception as e: 238 | logger.error(f"Error in component health check: {e}", exc_info=True) 239 | await asyncio.sleep(5) 240 | 241 | async def _monitor_memory(self, initial_snapshot: Any) -> None: 242 | last_snapshot = initial_snapshot 243 | while self.running: 244 | try: 245 | current_snapshot = tracemalloc.take_snapshot() 246 | diff_stats = current_snapshot.compare_to(last_snapshot, "lineno") 247 | significant = [stat for stat in diff_stats if abs(stat.size_diff) > 1024 * 1024] 248 | if significant: 249 | logger.warning("Significant memory changes detected:") 250 | for stat in significant[:3]: 251 | logger.warning(str(stat)) 252 | last_snapshot = current_snapshot 253 | await asyncio.sleep(self.configuration.MEMORY_CHECK_INTERVAL) 254 | except asyncio.CancelledError: 255 | logger.info("Memory monitoring cancelled.") 256 | break 257 | except Exception as e: 258 | logger.error(f"Memory monitoring error: {e}", exc_info=True) 259 | 260 | async def _process_profitable_transactions(self) -> None: 261 | while self.running: 262 | try: 263 | tx = await asyncio.wait_for( 264 | self.components["mempoolmonitor"].profitable_transactions.get(), 265 | timeout=self.configuration.PROFITABLE_TX_PROCESS_TIMEOUT 266 | ) 267 | strategy_type = tx.get("strategy_type", "unknown") 268 | success = await self.components["strategynet"].execute_best_strategy(tx, strategy_type) 269 | if success: 270 | logger.debug(f"Processed profitable tx: {tx.get('tx_hash','unknown')[:8]}...") 271 | self.components["mempoolmonitor"].profitable_transactions.task_done() 272 | except asyncio.TimeoutError: 273 | continue 274 | except Exception as e: 275 | logger.error(f"Error processing profitable transaction: {e}", exc_info=True) 276 | 277 | async def run(self) -> None: 278 | logger.info("Starting MainCore run loop...") 279 | self.running = True 280 | initial_snapshot = tracemalloc.take_snapshot() 281 | try: 282 | async with asyncio.TaskGroup() as tg: 283 | tg.create_task(self.components["mempoolmonitor"].start_monitoring()) 284 | tg.create_task(self._process_profitable_transactions()) 285 | tg.create_task(self._monitor_memory(initial_snapshot)) 286 | tg.create_task(self._check_component_health()) 287 | logger.info("All main tasks started.") 288 | except asyncio.CancelledError: 289 | logger.info("Main loop cancelled.") 290 | except Exception as e: 291 | logger.error(f"Fatal error in main loop: {e}", exc_info=True) 292 | finally: 293 | await self.stop() 294 | 295 | async def stop(self) -> None: 296 | if not self.running: 297 | return 298 | logger.warning("Initiating graceful shutdown...") 299 | self.running = False 300 | self._shutdown_event.set() 301 | stop_tasks = [] 302 | for name, comp in self.components.items(): 303 | if comp and hasattr(comp, "stop"): 304 | stop_tasks.append(asyncio.create_task(comp.stop())) 305 | if stop_tasks: 306 | await asyncio.gather(*stop_tasks, return_exceptions=True) 307 | if self.web3 and hasattr(self.web3.provider, "disconnect"): 308 | await self.web3.provider.disconnect() 309 | logger.info("Web3 provider disconnected.") 310 | self._log_final_memory_stats() 311 | tracemalloc.stop() 312 | logger.info("MainCore shutdown complete.") 313 | 314 | def _log_final_memory_stats(self) -> None: 315 | try: 316 | final_snapshot = tracemalloc.take_snapshot() 317 | stats = final_snapshot.compare_to(self.memory_snapshot, "lineno") 318 | logger.debug("Final memory allocation changes:") 319 | for stat in stats[:5]: 320 | logger.debug(str(stat)) 321 | except Exception as e: 322 | logger.error(f"Error logging memory stats: {e}", exc_info=True) 323 | 324 | async def emergency_shutdown(self) -> None: 325 | logger.warning("Emergency shutdown initiated...") 326 | self.running = False 327 | self._shutdown_event.set() 328 | for task in asyncio.all_tasks(): 329 | task.cancel() 330 | await asyncio.gather(*asyncio.all_tasks(), return_exceptions=True) 331 | if self.web3 and hasattr(self.web3.provider, "disconnect"): 332 | await self.web3.provider.disconnect() 333 | logger.info("Web3 provider disconnected.") 334 | self._log_final_memory_stats() 335 | tracemalloc.stop() 336 | logger.info("Emergency shutdown complete.") 337 | sys.exit(0) 338 | 339 | async def main() -> None: 340 | await run_bot() 341 | 342 | async def run_bot() -> None: 343 | loop = asyncio.get_running_loop() 344 | def shutdown_handler() -> None: 345 | logger.debug("Shutdown signal received; cancelling tasks...") 346 | for task in asyncio.all_tasks(loop): 347 | task.cancel() 348 | try: 349 | tracemalloc.start() 350 | await asyncio.sleep(3) 351 | configuration = Configuration() 352 | core = MainCore(configuration) 353 | for sig in (signal.SIGINT, signal.SIGTERM): 354 | loop.add_signal_handler(sig, shutdown_handler) 355 | await core.initialize_components() 356 | await core.run() 357 | except Exception as e: 358 | logger.critical(f"Fatal error in run_bot: {e}") 359 | finally: 360 | tracemalloc.stop() 361 | logger.debug("MainCore shutdown complete.") 362 | 363 | if __name__ == "__main__": 364 | try: 365 | asyncio.run(main()) 366 | except KeyboardInterrupt: 367 | pass 368 | except Exception as e: 369 | snapshot = tracemalloc.take_snapshot() 370 | logger.critical(f"Program terminated with error: {e}") 371 | stats = snapshot.statistics("lineno") 372 | for stat in stats[:10]: 373 | logger.debug(str(stat)) 374 | logger.debug("Program terminated.") 375 | -------------------------------------------------------------------------------- /python/marketmonitor.py: -------------------------------------------------------------------------------- 1 | # File: python/marketmonitor.py 2 | import asyncio 3 | import os 4 | import time 5 | import joblib 6 | import pandas as pd 7 | import numpy as np 8 | from datetime import datetime 9 | from typing import Any, Dict, List, Optional, Union 10 | from sklearn.linear_model import LinearRegression 11 | from cachetools import TTLCache 12 | from web3 import AsyncWeb3 13 | 14 | from apiconfig import APIConfig 15 | from configuration import Configuration 16 | from loggingconfig import setup_logging 17 | import logging 18 | 19 | logger = setup_logging("MarketMonitor", level=logging.DEBUG) 20 | 21 | 22 | class MarketMonitor: 23 | """ 24 | Monitors market data in real‑time and predicts price movement using a linear 25 | regression model. It periodically updates training data and retrains the 26 | model as needed. 27 | """ 28 | 29 | VOLATILITY_THRESHOLD: float = 0.05 # 5 % volatility threshold 30 | LIQUIDITY_THRESHOLD: float = 100_000 # Minimum volume threshold 31 | PRICE_EMA_SHORT_PERIOD: int = 12 32 | PRICE_EMA_LONG_PERIOD: int = 26 33 | 34 | def __init__( 35 | self, 36 | web3: AsyncWeb3, 37 | configuration: Configuration, 38 | apiconfig: APIConfig, 39 | transactioncore: Optional[Any] = None, 40 | ) -> None: 41 | 42 | self.web3 = web3 43 | self.configuration = configuration 44 | self.apiconfig = apiconfig 45 | self.transactioncore = transactioncore 46 | 47 | self.price_model: Optional[LinearRegression] = None 48 | self.last_training_time: float = 0.0 49 | self.model_accuracy: float = 0.0 50 | self.RETRAINING_INTERVAL: int = self.configuration.get_config_value( 51 | "MODEL_RETRAINING_INTERVAL", 3600 52 | ) 53 | self.MIN_TRAINING_SAMPLES: int = self.configuration.get_config_value( 54 | "MIN_TRAINING_SAMPLES", 100 55 | ) 56 | 57 | # Cache for recent market data 58 | self.price_cache: TTLCache = TTLCache(maxsize=2000, ttl=300) 59 | self.update_scheduler: Dict[str, float] = { 60 | "training_data": 0.0, 61 | "model": 0.0, 62 | "model_retraining_interval": self.RETRAINING_INTERVAL, 63 | } 64 | 65 | # Paths for the ML model and training data 66 | self.linear_regression_path: str = self.configuration.get_config_value( 67 | "LINEAR_REGRESSION_PATH", "linear_regression" 68 | ) 69 | self.model_path: str = self.configuration.get_config_value( 70 | "MODEL_PATH", "linear_regression/price_model.joblib" 71 | ) 72 | self.training_data_path: str = self.configuration.get_config_value( 73 | "TRAINING_DATA_PATH", "linear_regression/training_data.csv" 74 | ) 75 | 76 | # Ensure the training directory exists 77 | os.makedirs(self.linear_regression_path, exist_ok=True) 78 | 79 | # --------------------------------------------------------------------- # 80 | # INITIALISATION & SCHEDULING # 81 | # --------------------------------------------------------------------- # 82 | async def initialize(self) -> None: 83 | """ 84 | Load or create a price‑prediction model and any existing historical 85 | training data. Then start the periodic update tasks. 86 | """ 87 | try: 88 | # --- Model ---------------------------------------------------- # 89 | if os.path.exists(self.model_path): 90 | try: 91 | self.price_model = await asyncio.to_thread( 92 | joblib.load, self.model_path 93 | ) 94 | logger.debug("Loaded existing price model.") 95 | except Exception as e: 96 | logger.warning( 97 | f"Loading model failed ({e}); creating a new model." 98 | ) 99 | self.price_model = LinearRegression() 100 | await asyncio.to_thread(joblib.dump, self.price_model, self.model_path) 101 | else: 102 | self.price_model = LinearRegression() 103 | await asyncio.to_thread(joblib.dump, self.price_model, self.model_path) 104 | 105 | # --- Historical data ----------------------------------------- # 106 | if os.path.exists(self.training_data_path): 107 | try: 108 | self.historical_data = await asyncio.to_thread( 109 | pd.read_csv, self.training_data_path 110 | ) 111 | logger.debug( 112 | f"Loaded {len(self.historical_data)} training data points." 113 | ) 114 | except Exception as e: 115 | logger.warning(f"Failed to load training data: {e}") 116 | self.historical_data = pd.DataFrame() 117 | else: 118 | self.historical_data = pd.DataFrame() 119 | 120 | # --- Train model if we already have enough samples ------------ # 121 | if len(self.historical_data) >= self.MIN_TRAINING_SAMPLES: 122 | try: 123 | await self.train_price_model() 124 | logger.debug("Trained price model with available historical data.") 125 | except Exception as e: 126 | logger.warning(f"Model training failed: {e}") 127 | self.price_model = LinearRegression() 128 | await asyncio.to_thread(joblib.dump, self.price_model, self.model_path) 129 | self.historical_data = pd.DataFrame() 130 | else: 131 | logger.debug( 132 | "Insufficient historical data for training; model remains unchanged." 133 | ) 134 | 135 | logger.info("MarketMonitor initialized successfully.") 136 | asyncio.create_task(self.schedule_updates()) 137 | except Exception as e: 138 | logger.critical(f"MarketMonitor initialization failed: {e}", exc_info=True) 139 | raise 140 | 141 | async def schedule_updates(self) -> None: 142 | """ 143 | Periodically update training data and retrain the price model. 144 | """ 145 | while True: 146 | try: 147 | now = time.time() 148 | if ( 149 | now - self.update_scheduler["training_data"] 150 | >= self.update_scheduler["model_retraining_interval"] 151 | ): 152 | await self.update_training_data() 153 | self.update_scheduler["training_data"] = now 154 | 155 | if ( 156 | now - self.update_scheduler["model"] 157 | >= self.update_scheduler["model_retraining_interval"] 158 | ): 159 | await self.train_price_model() 160 | self.update_scheduler["model"] = now 161 | 162 | await asyncio.sleep(60) 163 | except asyncio.CancelledError: 164 | logger.info("MarketMonitor update scheduler cancelled.") 165 | break 166 | except Exception as e: 167 | logger.error(f"Error in update scheduler: {e}", exc_info=True) 168 | await asyncio.sleep(300) 169 | 170 | # --------------------------------------------------------------------- # 171 | # MARKET CONDITION ANALYSIS # 172 | # --------------------------------------------------------------------- # 173 | async def check_market_conditions(self, token_address: str) -> Dict[str, bool]: 174 | """ 175 | Evaluate market conditions for the given token address. 176 | """ 177 | conditions = { 178 | "high_volatility": False, 179 | "bullish_trend": False, 180 | "bearish_trend": False, 181 | "low_liquidity": False, 182 | } 183 | 184 | if not token_address: 185 | logger.debug("Token address is None or empty") 186 | return conditions 187 | 188 | symbol = self.apiconfig.get_token_symbol(token_address) 189 | if not symbol: 190 | logger.debug( 191 | f"Unable to determine token symbol for {token_address} in market condition check." 192 | ) 193 | return conditions 194 | 195 | try: 196 | prices = await self.apiconfig.get_token_price_data( 197 | symbol, "historical", timeframe=1, vs="usd" 198 | ) 199 | if not prices or len(prices) < 2: 200 | logger.debug(f"Not enough price data for {symbol}.") 201 | return conditions 202 | 203 | volatility = float(np.std(prices) / np.mean(prices)) 204 | if volatility > self.configuration.get_config_value( 205 | "VOLATILITY_THRESHOLD", self.VOLATILITY_THRESHOLD 206 | ): 207 | conditions["high_volatility"] = True 208 | 209 | avg_price = float(np.mean(prices)) 210 | if prices[-1] > avg_price: 211 | conditions["bullish_trend"] = True 212 | elif prices[-1] < avg_price: 213 | conditions["bearish_trend"] = True 214 | 215 | volume = await self.apiconfig.get_token_volume(symbol) 216 | if volume < self.configuration.get_config_value( 217 | "LIQUIDITY_THRESHOLD", self.LIQUIDITY_THRESHOLD 218 | ): 219 | conditions["low_liquidity"] = True 220 | 221 | logger.debug(f"Market conditions for {symbol}: {conditions}") 222 | except Exception as e: 223 | logger.error(f"Error checking market conditions for {symbol}: {e}", exc_info=True) 224 | 225 | return conditions 226 | 227 | # --------------------------------------------------------------------- # 228 | # PRICE PREDICTION # 229 | # --------------------------------------------------------------------- # 230 | async def predict_price_movement(self, token_symbol: str) -> float: 231 | """ 232 | Predict future price (USD) for the given token symbol. 233 | """ 234 | try: 235 | cache_key = f"prediction_{token_symbol}" 236 | if cache_key in self.apiconfig.prediction_cache: 237 | return self.apiconfig.prediction_cache[cache_key] 238 | 239 | prediction = await self.apiconfig.predict_price(token_symbol) 240 | self.apiconfig.prediction_cache[cache_key] = prediction 241 | return prediction 242 | except Exception as e: 243 | logger.error(f"Error predicting price for {token_symbol}: {e}") 244 | return 0.0 245 | 246 | # --------------------------------------------------------------------- # 247 | # SIMPLE WRAPPER AROUND APIConfig # 248 | # --------------------------------------------------------------------- # 249 | async def get_token_price_data( 250 | self, 251 | token_symbol: str, 252 | data_type: str = "current", 253 | timeframe: int = 1, 254 | vs: str = "eth", 255 | ) -> Union[float, List[float]]: 256 | """ 257 | Convenience wrapper around APIConfig.get_token_price_data(). 258 | """ 259 | return await self.apiconfig.get_token_price_data( 260 | token_symbol, data_type, timeframe, vs 261 | ) 262 | 263 | # --------------------------------------------------------------------- # 264 | # TRAINING‑DATA PIPELINE # 265 | # --------------------------------------------------------------------- # 266 | async def update_training_data(self) -> None: 267 | """ 268 | Append fresh market data to the CSV used for model training. 269 | """ 270 | logger.info("Updating training data...") 271 | training_file = self.training_data_path 272 | 273 | # Load existing data (if any) 274 | try: 275 | existing_df = pd.read_csv(training_file) 276 | except Exception: 277 | existing_df = pd.DataFrame() 278 | 279 | new_rows: List[Dict[str, Any]] = [] 280 | 281 | token_symbols = [ 282 | s 283 | for s in self.apiconfig.token_symbol_to_address.keys() 284 | if not s.startswith("_") and s.upper() != "_COMMENT" 285 | ] 286 | 287 | for token in token_symbols: 288 | try: 289 | prices: List[float] = await self.apiconfig.get_token_price_data( 290 | token, "historical", timeframe=1, vs="usd" 291 | ) 292 | if not prices: 293 | logger.debug(f"No historical prices for token {token}. Skipping.") 294 | continue 295 | 296 | current_price = float(prices[-1]) 297 | avg_price = float(np.mean(prices)) 298 | volatility = float(np.std(prices) / avg_price) if avg_price > 0 else 0.0 299 | percent_change_24h = ( 300 | (prices[-1] - prices[0]) / prices[0] * 100 if prices[0] else 0.0 301 | ) 302 | price_momentum = ( 303 | (prices[-1] - prices[0]) / prices[0] if prices[0] else 0.0 304 | ) 305 | 306 | volume_24h = await self.apiconfig.get_token_volume(token) 307 | metadata = await self.apiconfig.get_token_metadata(token) 308 | if not metadata: 309 | logger.debug(f"No metadata for token {token}. Skipping.") 310 | continue 311 | 312 | market_cap = metadata.get("market_cap", 0.0) 313 | total_supply = metadata.get("total_supply", 0.0) 314 | circulating_supply = metadata.get("circulating_supply", 0.0) 315 | trading_pairs = metadata.get("trading_pairs", 0) 316 | exchange_count = len(metadata.get("exchanges", [])) 317 | 318 | liquidity_ratio = volume_24h / market_cap if market_cap > 0 else 0.0 319 | 320 | new_rows.append( 321 | { 322 | "timestamp": int(datetime.utcnow().timestamp()), 323 | "symbol": token, 324 | "price_usd": current_price, 325 | "market_cap": market_cap, 326 | "volume_24h": volume_24h, 327 | "percent_change_24h": percent_change_24h, 328 | "total_supply": total_supply, 329 | "circulating_supply": circulating_supply, 330 | "volatility": volatility, 331 | "liquidity_ratio": liquidity_ratio, 332 | "avg_transaction_value": 0.0, 333 | "trading_pairs": trading_pairs, 334 | "exchange_count": exchange_count, 335 | "price_momentum": price_momentum, 336 | "buy_sell_ratio": 1.0, 337 | "smart_money_flow": 0.0, 338 | } 339 | ) 340 | logger.debug(f"Token {token} data appended.") 341 | except Exception as e: 342 | logger.error(f"Error updating training data for {token}: {e}", exc_info=True) 343 | 344 | if not new_rows: 345 | logger.info("No new training data was fetched.") 346 | return 347 | 348 | new_df = pd.DataFrame(new_rows) 349 | if not existing_df.empty: 350 | combined_df = pd.concat([existing_df, new_df], ignore_index=True) 351 | combined_df.drop_duplicates(subset=["timestamp", "symbol"], inplace=True) 352 | combined_df.sort_values("timestamp", inplace=True) 353 | else: 354 | combined_df = new_df 355 | 356 | combined_df.to_csv(training_file, index=False) 357 | logger.info( 358 | f"Training data updated with {len(new_rows)} new samples " 359 | f"(total {len(combined_df)})." 360 | ) 361 | 362 | # --------------------------------------------------------------------- # 363 | # MODEL TRAINING # 364 | # --------------------------------------------------------------------- # 365 | async def train_price_model(self) -> None: 366 | """ 367 | Train (or retrain) the linear‑regression price model from CSV data. 368 | """ 369 | try: 370 | if not os.path.exists(self.training_data_path): 371 | logger.warning("Training data file not found; skipping training.") 372 | return 373 | 374 | df = await asyncio.to_thread(pd.read_csv, self.training_data_path) 375 | if len(df) < self.MIN_TRAINING_SAMPLES: 376 | logger.warning( 377 | f"Insufficient training samples: {len(df)} " 378 | f"(required: {self.MIN_TRAINING_SAMPLES})." 379 | ) 380 | return 381 | 382 | features = [ 383 | "price_usd", 384 | "volume_24h", 385 | "market_cap", 386 | "volatility", 387 | "liquidity_ratio", 388 | "price_momentum", 389 | ] 390 | X = df[features].fillna(0) 391 | y = df["price_usd"].fillna(0) 392 | 393 | model = LinearRegression() 394 | model.fit(X, y) 395 | 396 | await asyncio.to_thread(joblib.dump, model, self.model_path) 397 | self.price_model = model 398 | 399 | logger.info(f"Price model trained and saved to {self.model_path}.") 400 | except Exception as e: 401 | logger.error(f"Error training price model: {e}", exc_info=True) 402 | 403 | # --------------------------------------------------------------------- # 404 | # SHUTDOWN # 405 | # --------------------------------------------------------------------- # 406 | async def stop(self) -> None: 407 | """ 408 | Flush caches and stop the MarketMonitor. 409 | """ 410 | try: 411 | self.price_cache.clear() 412 | logger.info("MarketMonitor stopped.") 413 | except Exception as e: 414 | logger.error(f"Error stopping MarketMonitor: {e}", exc_info=True) 415 | -------------------------------------------------------------------------------- /python/mempoolmonitor.py: -------------------------------------------------------------------------------- 1 | # python/mempoolmonitor.py 2 | 3 | import asyncio 4 | from typing import List, Any, Optional, Self 5 | from web3 import AsyncWeb3 6 | from web3.exceptions import TransactionNotFound 7 | from configuration import Configuration 8 | from safetynet import SafetyNet 9 | from noncecore import NonceCore 10 | from apiconfig import APIConfig 11 | from marketmonitor import MarketMonitor 12 | from loggingconfig import setup_logging 13 | import logging 14 | 15 | logger = setup_logging("MempoolMonitor", level=logging.DEBUG) 16 | 17 | class MempoolMonitor: 18 | """ 19 | Monitors the Ethereum mempool for pending transactions, processes them, and queues 20 | profitable transactions for further strategic evaluation. 21 | """ 22 | def __init__(self, 23 | web3: AsyncWeb3, 24 | safetynet: SafetyNet, 25 | noncecore: NonceCore, 26 | apiconfig: APIConfig, 27 | monitored_tokens: List[str], 28 | configuration: Configuration, 29 | marketmonitor: MarketMonitor): 30 | self.web3 = web3 31 | self.configuration = configuration 32 | self.safetynet = safetynet 33 | self.noncecore = noncecore 34 | self.apiconfig = apiconfig 35 | self.marketmonitor = marketmonitor 36 | # Accept both addresses and symbols, but store as normalized addresses for lookup 37 | self.monitored_tokens = set( 38 | apiconfig.get_token_address(t) if not t.startswith("0x") else t.lower() 39 | for t in monitored_tokens 40 | ) 41 | self.pending_transactions = asyncio.Queue() 42 | self.profitable_transactions = asyncio.Queue() 43 | self.task_queue = asyncio.PriorityQueue() 44 | self.processed_transactions = set() 45 | self.cache = {} 46 | self.backoff_factor = 1.5 47 | self.running = False 48 | 49 | async def initialize(self) -> None: 50 | """Reinitialize mempool monitoring queues and caches.""" 51 | self.running = False 52 | self.pending_transactions = asyncio.Queue() 53 | self.profitable_transactions = asyncio.Queue() 54 | self.task_queue = asyncio.PriorityQueue() 55 | self.processed_transactions.clear() 56 | self.cache.clear() 57 | logger.debug("MempoolMonitor initialized.") 58 | 59 | async def start_monitoring(self) -> None: 60 | """Start the mempool monitoring process.""" 61 | if self.running: 62 | logger.debug("MempoolMonitor is already running.") 63 | return 64 | self.running = True 65 | monitor_task = asyncio.create_task(self._run_monitoring()) 66 | processor_task = asyncio.create_task(self._process_task_queue()) 67 | logger.info("Mempool monitoring started.") 68 | await asyncio.gather(monitor_task, processor_task) 69 | 70 | async def _run_monitoring(self) -> None: 71 | """Attempt filter-based monitoring; if it fails, fallback to polling.""" 72 | while self.running: 73 | pending_filter = await self._setup_pending_filter() 74 | if pending_filter: 75 | try: 76 | while self.running: 77 | tx_hashes = await pending_filter.get_new_entries() 78 | if tx_hashes: 79 | await self._handle_new_transactions(tx_hashes) 80 | await asyncio.sleep(1) 81 | except Exception as e: 82 | logger.error(f"Error using filter-based monitoring: {e}") 83 | await asyncio.sleep(2) 84 | else: 85 | await self._poll_pending_transactions() 86 | 87 | async def _setup_pending_filter(self) -> Optional[Any]: 88 | """Set up a pending transaction filter if possible.""" 89 | try: 90 | pending_filter = await self.web3.eth.filter("pending") 91 | # Test the filter with a short timeout. 92 | await asyncio.wait_for(pending_filter.get_new_entries(), timeout=5) 93 | logger.debug("Using filter-based pending transactions.") 94 | return pending_filter 95 | except Exception as e: 96 | logger.warning(f"Pending filter unavailable: {e}. Falling back to polling.") 97 | return None 98 | 99 | async def _poll_pending_transactions(self) -> None: 100 | """Poll new blocks and process transactions.""" 101 | last_block = await self.web3.eth.block_number 102 | while self.running: 103 | try: 104 | current_block = await self.web3.eth.block_number 105 | if current_block <= last_block: 106 | await asyncio.sleep(1) 107 | continue 108 | for block_num in range(last_block + 1, current_block + 1): 109 | try: 110 | block = await self.web3.eth.get_block(block_num, full_transactions=True) 111 | if block and block.transactions: 112 | tx_hashes = [] 113 | for tx in block.transactions: 114 | # Support both dict and object types. 115 | if hasattr(tx, "hash"): 116 | tx_hashes.append(tx.hash.hex()) 117 | elif isinstance(tx, dict) and "hash" in tx: 118 | tx_hashes.append(tx["hash"].hex()) 119 | if tx_hashes: 120 | await self._handle_new_transactions(tx_hashes) 121 | except Exception as e: 122 | logger.error(f"Error processing block {block_num}: {e}") 123 | continue 124 | last_block = current_block 125 | await asyncio.sleep(1) 126 | except Exception as e: 127 | logger.error(f"Polling error: {e}") 128 | await asyncio.sleep(2) 129 | 130 | async def _handle_new_transactions(self, tx_hashes: List[str]) -> None: 131 | """Process new transaction hashes in a batch.""" 132 | for tx_hash in tx_hashes: 133 | await self._queue_transaction(tx_hash) 134 | 135 | async def _queue_transaction(self, tx_hash: str) -> None: 136 | """Queue a transaction if not already processed.""" 137 | if not tx_hash or tx_hash in self.processed_transactions: 138 | return 139 | self.processed_transactions.add(tx_hash) 140 | priority = await self._calculate_priority(tx_hash) 141 | await self.task_queue.put((priority, tx_hash)) 142 | 143 | async def _calculate_priority(self, tx_hash: str) -> int: 144 | """ 145 | Calculate a priority for the transaction based on its gas price. 146 | Higher gas price means higher priority (i.e. more negative). 147 | """ 148 | try: 149 | tx = await self._get_transaction_with_retry(tx_hash) 150 | if not tx: 151 | return float("inf") 152 | gas_price = tx.get("gasPrice", 0) 153 | return -gas_price 154 | except Exception as e: 155 | logger.error(f"Priority calculation error for {tx_hash}: {e}") 156 | return float("inf") 157 | 158 | async def _get_transaction_with_retry(self, tx_hash: str) -> dict: 159 | backoff = self.configuration.MEMPOOL_RETRY_DELAY 160 | retries = self.configuration.MEMPOOL_MAX_RETRIES 161 | for _ in range(retries): 162 | try: 163 | if tx_hash in self.cache: 164 | return self.cache[tx_hash] 165 | tx = await self.web3.eth.get_transaction(tx_hash) 166 | self.cache[tx_hash] = tx 167 | return tx 168 | except TransactionNotFound: 169 | await asyncio.sleep(backoff) 170 | backoff *= self.backoff_factor 171 | except Exception as e: 172 | logger.error(f"Error fetching transaction {tx_hash}: {e}") 173 | return {} 174 | return {} 175 | 176 | async def _process_task_queue(self) -> None: 177 | """Continuously process transactions from the priority queue.""" 178 | semaphore = asyncio.Semaphore(self.configuration.MEMPOOL_MAX_PARALLEL_TASKS) 179 | while self.running: 180 | try: 181 | priority, tx_hash = await self.task_queue.get() 182 | async with semaphore: 183 | asyncio.create_task(self.process_transaction(tx_hash)) 184 | except Exception as e: 185 | logger.error(f"Error in task queue processing: {e}") 186 | await asyncio.sleep(1) 187 | 188 | async def process_transaction(self, tx_hash: str) -> None: 189 | """Analyze a transaction and, if deemed profitable, add it to the profitable queue.""" 190 | try: 191 | tx = await self._get_transaction_with_retry(tx_hash) 192 | if not tx: 193 | return 194 | analysis = await self.analyze_transaction(tx) 195 | if analysis.get("is_profitable"): 196 | await self.profitable_transactions.put(analysis) 197 | logger.debug(f"Queued profitable transaction: {analysis.get('tx_hash')}") 198 | except Exception as e: 199 | logger.error(f"Error processing transaction {tx_hash}: {e}") 200 | 201 | async def analyze_transaction(self, tx: dict) -> dict: 202 | """ 203 | Analyze a transaction for profitability. 204 | This is a stub that you should extend with your actual analysis logic. 205 | """ 206 | result = {"is_profitable": False} 207 | try: 208 | # Use correct address/symbol mapping for token checks 209 | to_addr = tx.get("to", "") 210 | if to_addr: 211 | symbol = self.apiconfig.get_token_symbol(to_addr) 212 | else: 213 | symbol = None 214 | gas_price = tx.get("gasPrice", 0) 215 | value = tx.get("value", 0) 216 | # Only analyze if the transaction is for a monitored token 217 | if (gas_price > 0 and value > 0 and 218 | (to_addr.lower() in self.monitored_tokens or (symbol and symbol in self.apiconfig.token_symbol_to_address))): 219 | result = { 220 | "is_profitable": True, 221 | "tx_hash": tx.get("hash").hex() if "hash" in tx else "unknown", 222 | "gasPrice": gas_price, 223 | "value": value, 224 | "strategy_type": "front_run", 225 | "token_symbol": symbol, 226 | "token_address": to_addr 227 | } 228 | except Exception as e: 229 | logger.error(f"Analysis error: {e}") 230 | return result 231 | 232 | async def stop(self) -> None: 233 | """Stop the mempool monitor.""" 234 | self.running = False 235 | logger.info("MempoolMonitor stopped.") 236 | -------------------------------------------------------------------------------- /python/noncecore.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import time 3 | from typing import Any 4 | 5 | from cachetools import TTLCache 6 | from web3 import AsyncWeb3 7 | from web3.exceptions import Web3ValueError, TransactionNotFound 8 | from configuration import Configuration 9 | from loggingconfig import setup_logging 10 | import logging 11 | 12 | logger = setup_logging("NonceCore", level=logging.DEBUG) 13 | 14 | class NonceCore: 15 | """ 16 | Advanced nonce management system for Ethereum transactions with caching, 17 | auto-recovery, and comprehensive error handling. 18 | 19 | This module is critical to ensure that nonce assignment remains consistent 20 | and avoids transaction collisions. 21 | """ 22 | 23 | def __init__(self, web3: AsyncWeb3, address: str, configuration: Configuration) -> None: 24 | """ 25 | Initialize the NonceCore instance. 26 | 27 | Args: 28 | web3 (AsyncWeb3): An asynchronous Web3 instance. 29 | address (str): The Ethereum address for nonce management. 30 | configuration (Configuration): Configuration data. 31 | """ 32 | self.web3: AsyncWeb3 = web3 33 | self.configuration: Configuration = configuration 34 | self.address: str = address 35 | self.lock: asyncio.Lock = asyncio.Lock() 36 | self.nonce_cache: TTLCache = TTLCache(maxsize=1, ttl=self.configuration.NONCE_CACHE_TTL) 37 | self.last_sync: float = time.monotonic() 38 | self.pending_transactions: set[int] = set() 39 | self._initialized: bool = False 40 | 41 | async def initialize(self) -> None: 42 | """ 43 | Initialize nonce management with fallback mechanisms and error recovery. 44 | """ 45 | if self._initialized: 46 | logger.debug("NonceCore already initialized.") 47 | return 48 | try: 49 | await self._init_nonce() 50 | self._initialized = True 51 | logger.debug("NonceCore initialized successfully.") 52 | except Web3ValueError as e: 53 | logger.error(f"Web3ValueError during nonce initialization: {e}") 54 | raise 55 | except Exception as e: 56 | logger.error(f"Failed to initialize NonceCore: {e}", exc_info=True) 57 | raise 58 | 59 | async def _init_nonce(self) -> None: 60 | """ 61 | Fetch the current nonce from the chain and any pending nonce, 62 | then store the higher value in the cache. 63 | """ 64 | try: 65 | current_nonce = await self._fetch_current_nonce_with_retries() 66 | pending_nonce = await self._get_pending_nonce() 67 | self.nonce_cache[self.address] = max(current_nonce, pending_nonce) 68 | self.last_sync = time.monotonic() 69 | logger.debug(f"Initial nonce set to {self.nonce_cache[self.address]}") 70 | except Exception as e: 71 | logger.error(f"Error during nonce initialization: {e}", exc_info=True) 72 | raise 73 | 74 | async def get_nonce(self, force_refresh: bool = False) -> int: 75 | """ 76 | Retrieve the next available nonce, optionally forcing a refresh from chain. 77 | """ 78 | if not self._initialized: 79 | await self.initialize() 80 | if force_refresh or self._should_refresh_cache(): 81 | await self.refresh_nonce() 82 | return self.nonce_cache.get(self.address, 0) 83 | 84 | async def refresh_nonce(self) -> None: 85 | """ 86 | Refresh the nonce from the blockchain while using a lock to ensure consistency. 87 | """ 88 | async with self.lock: 89 | try: 90 | current_nonce = await self.web3.eth.get_transaction_count(self.address) 91 | self.nonce_cache[self.address] = current_nonce 92 | self.last_sync = time.monotonic() 93 | logger.debug(f"Nonce refreshed to {current_nonce}.") 94 | except Exception as e: 95 | logger.error(f"Error refreshing nonce: {e}", exc_info=True) 96 | 97 | async def _fetch_current_nonce_with_retries(self) -> int: 98 | """ 99 | Try to fetch the current nonce from the blockchain with exponential backoff. 100 | """ 101 | backoff = self.configuration.NONCE_RETRY_DELAY 102 | for attempt in range(self.configuration.NONCE_MAX_RETRIES): 103 | try: 104 | nonce = await self.web3.eth.get_transaction_count(self.address) 105 | logger.debug(f"Fetched current nonce: {nonce}") 106 | return nonce 107 | except Exception as e: 108 | logger.warning(f"Attempt {attempt+1} failed to fetch nonce: {e}. Retrying in {backoff} seconds...") 109 | await asyncio.sleep(backoff) 110 | backoff *= 2 111 | raise Web3ValueError("Failed to fetch current nonce after multiple retries") 112 | 113 | async def _get_pending_nonce(self) -> int: 114 | """ 115 | Retrieve the nonce from pending transactions. 116 | """ 117 | try: 118 | pending = await self.web3.eth.get_transaction_count(self.address, 'pending') 119 | logger.debug(f"Fetched pending nonce: {pending}") 120 | return pending 121 | except Exception as e: 122 | logger.error(f"Error fetching pending nonce: {e}", exc_info=True) 123 | raise Web3ValueError(f"Failed to fetch pending nonce: {e}") 124 | 125 | async def track_transaction(self, tx_hash: str, nonce: int) -> None: 126 | """ 127 | Track a transaction by adding its nonce to the pending set and later remove it. 128 | """ 129 | self.pending_transactions.add(nonce) 130 | try: 131 | receipt = await self.web3.eth.wait_for_transaction_receipt(tx_hash, timeout=self.configuration.NONCE_TRANSACTION_TIMEOUT) 132 | if receipt.status == 1: 133 | logger.info(f"Transaction {tx_hash} (Nonce: {nonce}) succeeded.") 134 | else: 135 | logger.error(f"Transaction {tx_hash} (Nonce: {nonce}) failed with status {receipt.status}.") 136 | except TransactionNotFound: 137 | logger.warning(f"Transaction {tx_hash} (Nonce: {nonce}) not found.") 138 | except asyncio.TimeoutError: 139 | logger.warning(f"Timeout waiting for transaction receipt of {tx_hash} (Nonce: {nonce}).") 140 | except Exception as e: 141 | logger.error(f"Error tracking transaction {tx_hash} (Nonce: {nonce}): {e}", exc_info=True) 142 | finally: 143 | self.pending_transactions.discard(nonce) 144 | 145 | async def sync_nonce_with_chain(self) -> None: 146 | """ 147 | Force a full synchronization with the blockchain’s nonce state. 148 | """ 149 | async with self.lock: 150 | await self.refresh_nonce() 151 | 152 | async def reset(self) -> None: 153 | """ 154 | Reset the nonce cache and pending transaction set. 155 | """ 156 | async with self.lock: 157 | self.nonce_cache.clear() 158 | self.pending_transactions.clear() 159 | await self.refresh_nonce() 160 | logger.debug("NonceCore reset successfully.") 161 | 162 | async def stop(self) -> None: 163 | """ 164 | Perform necessary shutdown actions for the nonce manager. 165 | """ 166 | if not self._initialized: 167 | return 168 | try: 169 | await self.reset() 170 | logger.info("NonceCore stopped successfully.") 171 | except Exception as e: 172 | logger.error(f"Error stopping NonceCore: {e}", exc_info=True) 173 | 174 | def _should_refresh_cache(self) -> bool: 175 | """ 176 | Determine if the nonce cache should be refreshed based on elapsed time. 177 | """ 178 | elapsed = time.monotonic() - self.last_sync 179 | return elapsed > self.configuration.NONCE_CACHE_TTL 180 | 181 | async def get_next_nonce(self) -> int: 182 | """ 183 | Retrieve the next nonce and increment the cached value. 184 | """ 185 | async with self.lock: 186 | current_nonce = await self.get_nonce() 187 | next_nonce = current_nonce + 1 188 | self.nonce_cache[self.address] = next_nonce 189 | return next_nonce 190 | -------------------------------------------------------------------------------- /python/pyutils/__init__.py: -------------------------------------------------------------------------------- 1 | # LICENSE: MIT // github.com/John0n1/ON1Builder 2 | 3 | """ 4 | ON1Builder package initialization. 5 | """ 6 | # ./ON1Builder/utils/Python/__init__.py 7 | 8 | from strategyexecutionerror import StrategyExecutionError 9 | 10 | __all__: list[str] = ['StrategyExecutionError'] 11 | 12 | def initialize_package() -> None: 13 | """ 14 | Initialize the ON1Builder package. 15 | """ 16 | try: 17 | # Add any package initialization logic here 18 | pass 19 | except Exception as e: 20 | raise RuntimeError(f"Failed to initialize ON1Builder package: {e}") 21 | 22 | def cleanup_package() -> None: 23 | """ 24 | Clean up resources used by the ON1Builder package. 25 | """ 26 | try: 27 | # Add any package cleanup logic here 28 | pass 29 | except Exception as e: 30 | raise RuntimeError(f"Failed to clean up ON1Builder package: {e}") 31 | -------------------------------------------------------------------------------- /python/pyutils/strategyexecutionerror.py: -------------------------------------------------------------------------------- 1 | #======================================================================================================================== 2 | # https://github.com/John0n1/ON1Builder 3 | 4 | # This file contains the StrategyExecutionError class, which is a custom exception for strategy execution failures. 5 | #======================================================================================================================== 6 | 7 | class StrategyExecutionError(Exception): 8 | """Custom exception for strategy execution failures.""" 9 | def __init__(self, message: str = "Strategy execution failed") -> None: 10 | self.message: str = message 11 | super().__init__(self.message) 12 | -------------------------------------------------------------------------------- /python/safetynet.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from decimal import Decimal 3 | from typing import Any, Dict, Tuple, Optional 4 | 5 | from cachetools import TTLCache 6 | from web3 import AsyncWeb3 7 | from eth_account import Account 8 | from web3.exceptions import Web3Exception 9 | 10 | from apiconfig import APIConfig 11 | from configuration import Configuration 12 | from marketmonitor import MarketMonitor 13 | 14 | from loggingconfig import setup_logging 15 | import logging 16 | 17 | logger = setup_logging("SafetyNet", level=logging.DEBUG) 18 | 19 | 20 | class SafetyNet: 21 | """ 22 | Provides risk management and transaction validation for MEV operations. 23 | It handles balance caching, profit verification, gas estimation, slippage adjustment, 24 | network congestion monitoring, and risk assessment. 25 | """ 26 | def __init__( 27 | self, 28 | web3: AsyncWeb3, 29 | configuration: Configuration, 30 | address: Optional[str] = None, 31 | account: Optional[Account] = None, 32 | apiconfig: Optional[APIConfig] = None, 33 | marketmonitor: Optional[MarketMonitor] = None, 34 | ) -> None: 35 | self.web3: AsyncWeb3 = web3 36 | self.configuration: Configuration = configuration 37 | self.address: Optional[str] = address 38 | self.account: Optional[Account] = account 39 | self.apiconfig: Optional[APIConfig] = apiconfig 40 | self.marketmonitor: Optional[MarketMonitor] = marketmonitor 41 | 42 | self.price_cache: TTLCache = TTLCache(maxsize=2000, ttl=self.configuration.SAFETYNET_CACHE_TTL) 43 | self.gas_price_cache: TTLCache = TTLCache(maxsize=1, ttl=self.configuration.SAFETYNET_GAS_PRICE_TTL) 44 | 45 | self.price_lock: asyncio.Lock = asyncio.Lock() 46 | 47 | logger.info("SafetyNet initialized and ready!") 48 | self.SLIPPAGE_CONFIG: Dict[str, float] = { 49 | "default": self.configuration.SLIPPAGE_DEFAULT, 50 | "min": self.configuration.MIN_SLIPPAGE, 51 | "max": self.configuration.MAX_SLIPPAGE, 52 | "high_congestion": self.configuration.SLIPPAGE_HIGH_CONGESTION, 53 | "low_congestion": self.configuration.SLIPPAGE_LOW_CONGESTION, 54 | } 55 | self.GAS_CONFIG: Dict[str, float] = { 56 | "max_gas_price_gwei": float(self.configuration.MAX_GAS_PRICE_GWEI), 57 | "min_profit_multiplier": self.configuration.MIN_PROFIT_MULTIPLIER, 58 | "base_gas_limit": self.configuration.BASE_GAS_LIMIT 59 | } 60 | 61 | async def initialize(self) -> None: 62 | """ 63 | Verify web3 connectivity and initialize SafetyNet. 64 | Raises: 65 | RuntimeError: If the web3 instance is not properly connected. 66 | """ 67 | try: 68 | if not self.web3: 69 | raise RuntimeError("Web3 instance is not provided in SafetyNet.") 70 | if not await self.web3.is_connected(): 71 | raise RuntimeError("Web3 connection failed in SafetyNet.") 72 | logger.debug("SafetyNet successfully connected to the blockchain.") 73 | except Exception as e: 74 | logger.critical(f"SafetyNet initialization failed: {e}", exc_info=True) 75 | raise 76 | 77 | async def get_balance(self, account: Account) -> Decimal: 78 | """ 79 | Retrieve the account balance in ETH with caching. 80 | """ 81 | cache_key = f"balance_{account.address}" 82 | if cache_key in self.price_cache: 83 | logger.debug("Balance retrieved from cache.") 84 | return self.price_cache[cache_key] 85 | 86 | for attempt in range(3): 87 | try: 88 | balance_wei = await self.web3.eth.get_balance(account.address) 89 | balance_eth = Decimal(self.web3.from_wei(balance_wei, "ether")) 90 | self.price_cache[cache_key] = balance_eth 91 | logger.debug(f"Fetched balance: {balance_eth} ETH") 92 | return balance_eth 93 | except Exception as e: 94 | logger.warning(f"Attempt {attempt+1} failed to fetch balance: {e}") 95 | await asyncio.sleep(2 ** attempt) 96 | logger.error("Failed to fetch balance after multiple retries.") 97 | return Decimal("0") 98 | 99 | async def ensure_profit( 100 | self, 101 | transaction_data: Dict[str, Any], 102 | minimum_profit_eth: Optional[float] = None, 103 | ) -> bool: 104 | """ 105 | Confirm a transaction yields sufficient profit after gas and slippage adjustments. 106 | 107 | Args: 108 | transaction_data (Dict[str, Any]): Details including output token, amounts, gas price, gas used. 109 | minimum_profit_eth (Optional[float]): Minimum required profit in ETH; defaults to config.MIN_PROFIT. 110 | 111 | Returns: 112 | bool: True if the estimated profit exceeds the threshold, else False. 113 | """ 114 | try: 115 | rt_price = await self.apiconfig.get_real_time_price(transaction_data["output_token"]) 116 | if rt_price is None: 117 | logger.warning("Real-time price unavailable; cannot ensure profit.") 118 | return False 119 | 120 | gas_price = Decimal(transaction_data["gas_price"]) 121 | gas_used = Decimal(transaction_data["gas_used"]) 122 | gas_cost = (gas_price * gas_used * Decimal("1e-9")).quantize(Decimal("0.000000001")) 123 | 124 | slippage = await self.adjust_slippage_tolerance() 125 | profit = await self._calculate_profit(transaction_data, rt_price, slippage, gas_cost) 126 | self._log_profit_calculation(transaction_data, rt_price, gas_cost, profit, minimum_profit_eth or self.configuration.MIN_PROFIT) 127 | return profit > Decimal(minimum_profit_eth or self.configuration.MIN_PROFIT) 128 | except KeyError as e: 129 | logger.error(f"Missing required key in transaction data: {e}") 130 | return False 131 | except Exception as e: 132 | logger.error(f"Error in ensure_profit: {e}", exc_info=True) 133 | return False 134 | 135 | def _validate_gas_parameters(self, gas_price_gwei: Decimal, gas_used: Decimal) -> bool: 136 | """ 137 | Validate that gas parameters are within safe thresholds. 138 | """ 139 | if gas_used <= 0: 140 | logger.error("Gas used must be greater than zero.") 141 | return False 142 | if gas_price_gwei > Decimal(self.GAS_CONFIG["max_gas_price_gwei"]): 143 | logger.warning(f"Gas price {gas_price_gwei} Gwei exceeds allowed maximum.") 144 | return False 145 | return True 146 | 147 | def _calculate_gas_cost(self, gas_price_gwei: Decimal, gas_used: Decimal) -> Decimal: 148 | """ 149 | Calculate total gas cost in ETH using a fixed conversion. 150 | """ 151 | return (gas_price_gwei * gas_used * Decimal("1e-9")).quantize(Decimal("0.000000001")) 152 | 153 | async def _calculate_profit( 154 | self, 155 | transaction_data: Dict[str, Any], 156 | real_time_price: Decimal, 157 | slippage: float, 158 | gas_cost: Decimal, 159 | ) -> Decimal: 160 | """ 161 | Calculate expected profit: Adjust expected output by slippage, then subtract input amount and gas cost. 162 | """ 163 | try: 164 | expected_output = real_time_price * Decimal(transaction_data["amountOut"]) 165 | input_amount = Decimal(transaction_data["amountIn"]) 166 | adjusted_output = expected_output * (1 - Decimal(slippage)) 167 | profit = adjusted_output - input_amount - gas_cost 168 | return profit.quantize(Decimal("0.000000001")) 169 | except Exception as e: 170 | logger.error(f"Error calculating profit: {e}", exc_info=True) 171 | return Decimal("0") 172 | 173 | def _log_profit_calculation( 174 | self, 175 | tx_data: Dict[str, Any], 176 | real_time_price: Decimal, 177 | gas_cost: Decimal, 178 | profit: Decimal, 179 | min_profit: float 180 | ) -> None: 181 | """ 182 | Log details of profit calculation. 183 | """ 184 | profitable = "Yes" if profit > Decimal(min_profit) else "No" 185 | logger.debug( 186 | f"Profit Calculation Summary:\n" 187 | f" Token: {tx_data.get('output_token')}\n" 188 | f" Real-time Price: {real_time_price:.6f} ETH\n" 189 | f" Input Amount: {Decimal(tx_data.get('amountIn')):.6f} ETH\n" 190 | f" Expected Output: {Decimal(tx_data.get('amountOut')):.6f}\n" 191 | f" Gas Cost: {gas_cost:.6f} ETH\n" 192 | f" Calculated Profit: {profit:.6f} ETH\n" 193 | f" Minimum Required Profit: {min_profit} ETH\n" 194 | f" Profitable: {profitable}" 195 | ) 196 | 197 | async def estimate_gas(self, transaction_data: Dict[str, Any]) -> int: 198 | """ 199 | Estimate gas for a transaction. 200 | """ 201 | try: 202 | gas_estimate = await self.web3.eth.estimate_gas(transaction_data) 203 | return gas_estimate 204 | except Exception as e: 205 | logger.error(f"Gas estimation failed: {e}", exc_info=True) 206 | return self.GAS_CONFIG["base_gas_limit"] 207 | 208 | async def adjust_slippage_tolerance(self) -> float: 209 | """ 210 | Adjust slippage based on network congestion. 211 | """ 212 | try: 213 | congestion = await self.get_network_congestion() 214 | if congestion > 0.8: 215 | slippage = self.SLIPPAGE_CONFIG["high_congestion"] 216 | elif congestion < 0.2: 217 | slippage = self.SLIPPAGE_CONFIG["low_congestion"] 218 | else: 219 | slippage = self.SLIPPAGE_CONFIG["default"] 220 | # Ensure slippage falls between min and max. 221 | slippage = max(self.SLIPPAGE_CONFIG["min"], min(slippage, self.SLIPPAGE_CONFIG["max"])) 222 | logger.debug(f"Adjusted slippage to {slippage * 100:.2f}% based on network congestion {congestion*100:.2f}%") 223 | return slippage 224 | except Exception as e: 225 | logger.error(f"Error adjusting slippage tolerance: {e}", exc_info=True) 226 | return self.SLIPPAGE_CONFIG["default"] 227 | 228 | async def get_network_congestion(self) -> float: 229 | """ 230 | Estimate network congestion based on the latest block's gas usage. 231 | """ 232 | try: 233 | latest_block = await self.web3.eth.get_block("latest") 234 | gas_used = Decimal(latest_block["gasUsed"]) 235 | gas_limit = Decimal(latest_block["gasLimit"]) 236 | congestion = float((gas_used / gas_limit).quantize(Decimal("0.0001"))) 237 | logger.debug(f"Network congestion: {congestion*100:.2f}%") 238 | return congestion 239 | except Exception as e: 240 | logger.error(f"Error fetching network congestion: {e}", exc_info=True) 241 | return 0.5 242 | 243 | async def check_transaction_safety(self, tx_data: Dict[str, Any], check_type: str = "all") -> Tuple[bool, Dict[str, Any]]: 244 | """ 245 | Check transaction safety by validating gas price, estimated profit, and network congestion. 246 | Returns a tuple (is_safe, details). 247 | """ 248 | try: 249 | messages = [] 250 | gas_ok = True 251 | profit_ok = True 252 | congestion_ok = True 253 | 254 | if check_type in ("all", "gas"): 255 | current_gas = await self.get_dynamic_gas_price() 256 | if current_gas > Decimal(self.configuration.MAX_GAS_PRICE_GWEI): 257 | gas_ok = False 258 | messages.append(f"Gas price {current_gas} Gwei exceeds limit {self.configuration.MAX_GAS_PRICE_GWEI} Gwei.") 259 | 260 | if check_type in ("all", "profit"): 261 | current_price = await self.apiconfig.get_real_time_price(tx_data["output_token"]) 262 | slippage = await self.adjust_slippage_tolerance() 263 | gas_cost = self._calculate_gas_cost(Decimal(tx_data["gas_price"]), Decimal(tx_data["gas_used"])) 264 | profit = await self._calculate_profit(tx_data, current_price, slippage, gas_cost) 265 | if profit < Decimal(self.configuration.MIN_PROFIT): 266 | profit_ok = False 267 | messages.append(f"Calculated profit {profit:.6f} ETH below threshold {self.configuration.MIN_PROFIT} ETH.") 268 | 269 | if check_type in ("all", "network"): 270 | congestion = await self.get_network_congestion() 271 | if congestion > 0.8: 272 | congestion_ok = False 273 | messages.append(f"Network congestion too high: {congestion*100:.2f}%.") 274 | 275 | is_safe = gas_ok and profit_ok and congestion_ok 276 | return is_safe, { 277 | "is_safe": is_safe, 278 | "gas_ok": gas_ok, 279 | "profit_ok": profit_ok, 280 | "congestion_ok": congestion_ok, 281 | "messages": messages 282 | } 283 | except Exception as e: 284 | logger.error(f"Error in transaction safety check: {e}", exc_info=True) 285 | return False, {"is_safe": False, "messages": [str(e)]} 286 | 287 | async def get_dynamic_gas_price(self) -> Decimal: 288 | """ 289 | Fetch dynamic gas price in Gwei, using caching to minimize network calls. 290 | """ 291 | if "gas_price" in self.gas_price_cache: 292 | return self.gas_price_cache["gas_price"] 293 | try: 294 | latest_block = await self.web3.eth.get_block("latest") 295 | base_fee = latest_block.get("baseFeePerGas") 296 | if base_fee: 297 | gas_price_wei = base_fee * 2 298 | gas_price_gwei = Decimal(str(self.web3.from_wei(gas_price_wei, "gwei"))) 299 | else: 300 | raw_gas_price = await self.web3.eth.gas_price 301 | gas_price_gwei = Decimal(str(self.web3.from_wei(raw_gas_price, "gwei"))) 302 | self.gas_price_cache["gas_price"] = gas_price_gwei 303 | logger.debug(f"Dynamic gas price fetched: {gas_price_gwei} Gwei") 304 | return gas_price_gwei 305 | except Exception as e: 306 | logger.error(f"Error fetching dynamic gas price: {e}", exc_info=True) 307 | return Decimal(self.configuration.MAX_GAS_PRICE_GWEI) 308 | 309 | async def validate_transaction(self, tx_data: Dict[str, Any]) -> bool: 310 | """ 311 | Validate that tx_data contains required fields and safe gas parameters. 312 | """ 313 | try: 314 | required_fields = ["output_token", "amountOut", "amountIn", "gas_price", "gas_used"] 315 | for field in required_fields: 316 | if field not in tx_data: 317 | logger.error(f"Missing field in tx_data: {field}") 318 | return False 319 | 320 | gas_price = Decimal(tx_data["gas_price"]) 321 | gas_used = Decimal(tx_data["gas_used"]) 322 | if not self._validate_gas_parameters(gas_price, gas_used): 323 | return False 324 | return True 325 | except Exception as e: 326 | logger.error(f"Error during transaction validation: {e}", exc_info=True) 327 | return False 328 | 329 | async def stop(self) -> None: 330 | """ 331 | Gracefully stop SafetyNet operations. 332 | """ 333 | try: 334 | if self.apiconfig: 335 | await self.apiconfig.close() 336 | logger.info("SafetyNet stopped successfully.") 337 | except Exception as e: 338 | logger.error(f"Error stopping SafetyNet: {e}", exc_info=True) 339 | raise 340 | -------------------------------------------------------------------------------- /python/strategynet.py: -------------------------------------------------------------------------------- 1 | # File: python/strategynet.py 2 | 3 | import asyncio 4 | import time 5 | import numpy as np 6 | import random 7 | from typing import Any, Dict, List, Callable 8 | from decimal import Decimal 9 | 10 | from apiconfig import APIConfig 11 | from transactioncore import TransactionCore 12 | from safetynet import SafetyNet 13 | from marketmonitor import MarketMonitor 14 | from loggingconfig import setup_logging 15 | import logging 16 | 17 | logger = setup_logging("StrategyNet", level=logging.DEBUG) 18 | 19 | class StrategyPerformanceMetrics: 20 | def __init__(self): 21 | self.successes: int = 0 22 | self.failures: int = 0 23 | self.profit: Decimal = Decimal("0") 24 | self.avg_execution_time: float = 0.0 25 | self.success_rate: float = 0.0 26 | self.total_executions: int = 0 27 | 28 | class StrategyConfiguration: 29 | def __init__(self): 30 | self.decay_factor: float = 0.95 31 | self.learning_rate: float = 0.01 32 | self.exploration_rate: float = 0.1 33 | self.FRONT_RUN_OPPORTUNITY_SCORE_THRESHOLD: int = 75 34 | self.VOLATILITY_FRONT_RUN_SCORE_THRESHOLD: int = 75 35 | self.AGGRESSIVE_FRONT_RUN_RISK_SCORE_THRESHOLD: float = 0.7 36 | 37 | class StrategyNet: 38 | """ 39 | Orchestrates the selection and execution of strategies using reinforcement learning weights. 40 | It does not implement the trading actions itself, but instead calls into TransactionCore. 41 | """ 42 | def __init__(self, 43 | transactioncore: TransactionCore, 44 | marketmonitor: MarketMonitor, 45 | safetynet: SafetyNet, 46 | apiconfig: APIConfig) -> None: 47 | self.transactioncore = transactioncore 48 | self.marketmonitor = marketmonitor 49 | self.safetynet = safetynet 50 | self.apiconfig = apiconfig 51 | self.strategy_types = ["eth_transaction", "front_run", "back_run", "sandwich_attack"] 52 | self._strategy_registry: Dict[str, List[Callable[[Dict[str, Any]], asyncio.Future]]] = { 53 | "eth_transaction": [self.transactioncore.handle_eth_transaction], 54 | "front_run": [ 55 | self.transactioncore.front_run, 56 | self.transactioncore.aggressive_front_run, 57 | self.transactioncore.predictive_front_run, 58 | self.transactioncore.volatility_front_run 59 | ], 60 | "back_run": [ 61 | self.transactioncore.back_run, 62 | self.transactioncore.price_dip_back_run, 63 | self.transactioncore.flashloan_back_run, 64 | self.transactioncore.high_volume_back_run 65 | ], 66 | "sandwich_attack": [ 67 | self.transactioncore.execute_sandwich_attack 68 | ] 69 | } 70 | # Initialize performance metrics and reinforcement weights. 71 | self.strategy_performance: Dict[str, StrategyPerformanceMetrics] = { 72 | stype: StrategyPerformanceMetrics() for stype in self.strategy_types 73 | } 74 | # Start with equal weights for all strategies per type. 75 | self.reinforcement_weights: Dict[str, np.ndarray] = { 76 | stype: np.ones(len(self._strategy_registry[stype])) 77 | for stype in self.strategy_types 78 | } 79 | self.configuration: StrategyConfiguration = StrategyConfiguration() 80 | logger.debug("StrategyNet initialized.") 81 | 82 | async def initialize(self) -> None: 83 | # For production, additional initialization could be done here. 84 | logger.info("StrategyNet initialization complete.") 85 | 86 | def get_strategies(self, strategy_type: str) -> List[Callable[[Dict[str, Any]], asyncio.Future]]: 87 | return self._strategy_registry.get(strategy_type, []) 88 | 89 | async def _select_best_strategy(self, strategies: List[Callable[[Dict[str, Any]], asyncio.Future]], strategy_type: str) -> Callable[[Dict[str, Any]], asyncio.Future]: 90 | weights = self.reinforcement_weights[strategy_type] 91 | if random.random() < self.configuration.exploration_rate: 92 | logger.debug("Exploration: randomly selecting a strategy.") 93 | return random.choice(strategies) 94 | max_weight = np.max(weights) 95 | exp_weights = np.exp(weights - max_weight) # for numerical stability 96 | probabilities = exp_weights / exp_weights.sum() 97 | selected_index = np.random.choice(len(strategies), p=probabilities) 98 | selected_strategy = strategies[selected_index] 99 | logger.debug(f"Selected strategy '{selected_strategy.__name__}' (weight: {weights[selected_index]:.4f}).") 100 | return selected_strategy 101 | 102 | def _calculate_reward(self, success: bool, profit: Decimal, execution_time: float) -> float: 103 | """ 104 | Compute the reward of a strategy execution. 105 | Rewards are based on profit (if successful) minus penalties for slow execution and high risk. 106 | """ 107 | # Base reward: profit in ETH if successful; a small negative base if not 108 | base_reward = float(profit) if success else -0.1 109 | # Time penalty: larger execution times incur a penalty 110 | time_penalty = 0.01 * execution_time 111 | # Risk penalty: a fixed penalty representing inherent variability 112 | risk_penalty = 0.05 # In a real system, compute variance from historical execution times 113 | # Worst-case penalty: additional penalty if execution_time exceeds a threshold (e.g., 2 seconds) 114 | worst_case_threshold = 2.0 115 | worst_case_penalty = 0.05 * max(0.0, execution_time - worst_case_threshold) 116 | total_reward = base_reward - time_penalty - risk_penalty - worst_case_penalty 117 | logger.debug(f"Reward computed: base: {base_reward:.4f}, time_penalty: {time_penalty:.4f}, " 118 | f"risk_penalty: {risk_penalty:.4f}, worst_case_penalty: {worst_case_penalty:.4f}, " 119 | f"total: {total_reward:.4f}") 120 | return total_reward 121 | 122 | async def _update_strategy_metrics(self, 123 | strategy_name: str, 124 | strategy_type: str, 125 | success: bool, 126 | profit: Decimal, 127 | execution_time: float) -> None: 128 | metrics = self.strategy_performance[strategy_type] 129 | metrics.total_executions += 1 130 | if success: 131 | metrics.successes += 1 132 | metrics.profit += profit 133 | else: 134 | metrics.failures += 1 135 | 136 | # Update average execution time using an exponential moving average 137 | decay = self.configuration.decay_factor 138 | metrics.avg_execution_time = (metrics.avg_execution_time * decay + 139 | execution_time * (1 - decay)) 140 | metrics.success_rate = metrics.successes / metrics.total_executions 141 | 142 | # Determine the index of the strategy 143 | strategy_index = self.get_strategy_index(strategy_name, strategy_type) 144 | if strategy_index >= 0: 145 | current_weight = self.reinforcement_weights[strategy_type][strategy_index] 146 | reward = self._calculate_reward(success, profit, execution_time) 147 | # Q-learning inspired update: using a discount factor (gamma) and learning rate 148 | gamma = 0.9 # discount factor 149 | # For this design, we use the current weight as the next maximum; in a complete system, you’d compute next max Q. 150 | next_max_q = current_weight 151 | learning_rate = self.configuration.learning_rate 152 | updated_weight = current_weight + learning_rate * (reward + gamma * next_max_q - current_weight) 153 | self.reinforcement_weights[strategy_type][strategy_index] = max(0.1, updated_weight) 154 | logger.debug(f"Updated weight for {strategy_name} (type: {strategy_type}, index: {strategy_index}) " 155 | f"from {current_weight:.4f} to {updated_weight:.4f}") 156 | logger.debug(f"Strategy metrics for {strategy_type}: total_executions: {metrics.total_executions}, " 157 | f"success_rate: {metrics.success_rate:.4f}, avg_execution_time: {metrics.avg_execution_time:.4f}") 158 | 159 | def get_strategy_index(self, strategy_name: str, strategy_type: str) -> int: 160 | strategies = self.get_strategies(strategy_type) 161 | for idx, strat in enumerate(strategies): 162 | if strat.__name__ == strategy_name: 163 | return idx 164 | logger.warning(f"Strategy {strategy_name} not found for type {strategy_type}") 165 | return -1 166 | 167 | async def execute_best_strategy(self, target_tx: Dict[str, Any], strategy_type: str) -> bool: 168 | strategies = self.get_strategies(strategy_type) 169 | if not strategies: 170 | logger.debug(f"No strategies available for type {strategy_type}.") 171 | return False 172 | start_time = time.time() 173 | selected_strategy = await self._select_best_strategy(strategies, strategy_type) 174 | profit_before = Decimal(self.transactioncore.current_profit) 175 | success = await selected_strategy(target_tx) 176 | profit_after = Decimal(self.transactioncore.current_profit) 177 | execution_time = time.time() - start_time 178 | profit_made = profit_after - profit_before 179 | await self._update_strategy_metrics(selected_strategy.__name__, strategy_type, success, profit_made, execution_time) 180 | return success 181 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiofiles==24.1.0 2 | aiohappyeyeballs==2.6.1 3 | aiohttp==3.11.18 4 | aiosignal==1.3.2 5 | annotated-types==0.7.0 6 | async-timeout==5.0.1 7 | attrs==25.3.0 8 | bitarray==3.3.1 9 | cachetools==5.5.2 10 | certifi==2025.1.31 11 | charset-normalizer==3.4.1 12 | ckzg==2.1.1 13 | colorama==0.4.6 14 | colorlog==6.9.0 15 | cytoolz==1.0.1 16 | eth-account==0.13.7 17 | eth-hash==0.7.1 18 | eth-rlp==2.2.0 19 | eth-typing==5.2.1 20 | eth-utils==5.3.0 21 | eth_abi==5.2.0 22 | frozenlist==1.6.0 23 | hexbytes==1.3.0 24 | idna==3.10 25 | iniconfig==2.1.0 26 | joblib==1.4.2 27 | multidict==6.4.3 28 | mypy==1.15.0 29 | mypy-extensions==1.1.0 30 | numpy==2.2.4 31 | packaging==25.0 32 | pandas==2.2.3 33 | parsimonious==0.10.0 34 | pluggy==1.5.0 35 | propcache==0.3.1 36 | pycryptodome==3.22.0 37 | pydantic==2.11.3 38 | pydantic_core==2.33.1 39 | pytest==8.3.5 40 | python-dateutil==2.9.0.post0 41 | python-dotenv==1.1.0 42 | pytz==2025.2 43 | pyunormalize==16.0.0 44 | regex==2024.11.6 45 | requests==2.32.3 46 | rlp==4.1.0 47 | scheduling==0.0.2 48 | scikit-learn==1.6.1 49 | scipy==1.15.2 50 | six==1.17.0 51 | threadpoolctl==3.6.0 52 | toolz==1.0.0 53 | types-requests==2.32.0.20250328 54 | typing-inspection==0.4.0 55 | typing_extensions==4.13.2 56 | tzdata==2025.2 57 | urllib3==2.4.0 58 | utils==1.0.2 59 | web3==7.10.0 60 | websockets==15.0.1 61 | yarl==1.20.0 62 | flask==3.1.0 63 | flask-cors==5.0.1 64 | flask_socketio==5.5.1 65 | -------------------------------------------------------------------------------- /template.env: -------------------------------------------------------------------------------- 1 | # --------------------- General Settings --------------------- 2 | # Maximum gas price (Wei) and transaction parameters 3 | MAX_GAS_PRICE=100000000000 4 | GAS_LIMIT=1000000 5 | MAX_SLIPPAGE=0.01 6 | MIN_PROFIT=0.001 7 | MIN_BALANCE=0.000001 8 | MEMORY_CHECK_INTERVAL=300 9 | COMPONENT_HEALTH_CHECK_INTERVAL=60 10 | PROFITABLE_TX_PROCESS_TIMEOUT=1.0 11 | 12 | # --------------------- Standard Addresses --------------------- 13 | # Standard token addresses on Ethereum Mainnet 14 | WETH_ADDRESS=0xC02aaa39b223FE8D0a0e5C4F27eAD9083C756Cc2 15 | USDC_ADDRESS=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 16 | USDT_ADDRESS=0xdAC17F958D2ee523a2206206994597C13D831ec7 17 | 18 | # --------------------- API Keys and Endpoints --------------------- 19 | # API keys: Replace with your keys. 20 | ETHERSCAN_API_KEY=etherscan_api_key 21 | INFURA_PROJECT_ID=infura_project 22 | INFURA_API_KEY=infura_api_key 23 | COINGECKO_API_KEY=coingecko_api_key 24 | COINMARKETCAP_API_KEY=coinmarketcap_api_key 25 | CRYPTOCOMPARE_API_KEY=cryptocompare_api_key 26 | 27 | # Ethereum node endpoints: At least one required! 28 | HTTP_ENDPOINT=http://127.0.0.1:8545 29 | WEBSOCKET_ENDPOINT=ws://127.0.0.1:8546 30 | IPC_ENDPOINT=/path/to/geth.ipc 31 | 32 | # --------------------- Account Configuration --------------------- 33 | # Your wallet details: Replace these with your own wallet address and private key! 34 | WALLET_ADDRESS=0xYourEthereumAddress 35 | WALLET_KEY=0xYourPrivateKey 36 | 37 | # --------------------- File Paths --------------------- 38 | # Relative paths to ABI and config files 39 | ERC20_ABI=abi/erc20_abi.json 40 | AAVE_FLASHLOAN_ABI=abi/aave_flashloan_abi.json 41 | AAVE_POOL_ABI=abi/aave_pool_abi.json 42 | UNISWAP_ABI=abi/uniswap_abi.json 43 | SUSHISWAP_ABI=abi/sushiswap_abi.json 44 | ERC20_SIGNATURES=abi/erc20_signatures.json 45 | TOKEN_ADDRESSES=utils/token_addresses.json 46 | TOKEN_SYMBOLS=utils/token_symbols.json 47 | GAS_PRICE_ORACLE_ABI=abi/gas_price_oracle_abi.json 48 | 49 | MEV_BUILDERS='[{"name": "flashbots", "url": "https://relay.flashbots.net", "auth_header": "X-Flashbots-Signature"}]' 50 | 51 | # --------------------- Router Addresses --------------------- 52 | # Addresses for decentralized exchange routers and lending protocols. 53 | UNISWAP_ADDRESS=0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D 54 | SUSHISWAP_ADDRESS=0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F 55 | # Aave Pool: Mainnet address 56 | AAVE_POOL_ADDRESS=0xb53c1a33016b2dc2ff3653530bff1848a515c8c5 57 | # Flashloan contract: Replace with your deployed flashloan contract address. 58 | AAVE_FLASHLOAN_ADDRESS=0xYourAaveFlashloanAddress 59 | # Gas Price Oracle: Replace with your deployed oracle address. 60 | GAS_PRICE_ORACLE_ADDRESS=0xYourGasPriceOracleAddress 61 | 62 | # --------------------- Slippage and Gas Configuration --------------------- 63 | # Slippage tolerances (expressed as fractions) and gas parameters. 64 | SLIPPAGE_DEFAULT=0.1 65 | MIN_SLIPPAGE=0.01 66 | MAX_SLIPPAGE=0.5 67 | SLIPPAGE_HIGH_CONGESTION=0.05 68 | SLIPPAGE_LOW_CONGESTION=0.2 69 | MAX_GAS_PRICE_GWEI=500 70 | MIN_PROFIT_MULTIPLIER=2.0 71 | BASE_GAS_LIMIT=21000 72 | DEFAULT_CANCEL_GAS_PRICE_GWEI=60 73 | ETH_TX_GAS_PRICE_MULTIPLIER=1.2 74 | 75 | # --------------------- ML Model Configuration --------------------- 76 | # Settings for model retraining and prediction. 77 | MODEL_RETRAINING_INTERVAL=3600 78 | MIN_TRAINING_SAMPLES=100 79 | MODEL_ACCURACY_THRESHOLD=0.7 80 | PREDICTION_CACHE_TTL=300 81 | LINEAR_REGRESSION_PATH=linear_regression 82 | MODEL_PATH=linear_regression/price_model.joblib 83 | TRAINING_DATA_PATH=linear_regression/training_data.csv 84 | 85 | # --------------------- Mempool Monitor Configuration --------------------- 86 | # Parameters for mempool monitoring. 87 | MEMPOOL_MAX_RETRIES=3 88 | MEMPOOL_RETRY_DELAY=2 89 | MEMPOOL_BATCH_SIZE=10 90 | MEMPOOL_MAX_PARALLEL_TASKS=5 91 | 92 | # --------------------- Nonce Core Configuration --------------------- 93 | # Nonce management parameters. 94 | NONCE_CACHE_TTL=60 95 | NONCE_RETRY_DELAY=1 96 | NONCE_MAX_RETRIES=5 97 | NONCE_TRANSACTION_TIMEOUT=120 98 | 99 | # --------------------- Safety Net Configuration --------------------- 100 | # Safety checks and profit verification. 101 | SAFETYNET_CACHE_TTL=300 102 | SAFETYNET_GAS_PRICE_TTL=30 103 | 104 | # --------------------- Strategy Net Configuration --------------------- 105 | # Strategy thresholds for various MEV tactics. 106 | AGGRESSIVE_FRONT_RUN_MIN_VALUE_ETH=0.1 107 | AGGRESSIVE_FRONT_RUN_RISK_SCORE_THRESHOLD=0.7 108 | FRONT_RUN_OPPORTUNITY_SCORE_THRESHOLD=75 109 | VOLATILITY_FRONT_RUN_SCORE_THRESHOLD=75 110 | ADVANCED_FRONT_RUN_RISK_SCORE_THRESHOLD=75 111 | PRICE_DIP_BACK_RUN_THRESHOLD=0.99 112 | FLASHLOAN_BACK_RUN_PROFIT_PERCENTAGE=0.02 113 | HIGH_VOLUME_BACK_RUN_DEFAULT_THRESHOLD_USD=100000 114 | SANDWICH_ATTACK_GAS_PRICE_THRESHOLD_GWEI=200 115 | PRICE_BOOST_SANDWICH_MOMENTUM_THRESHOLD=0.02 116 | 117 | # --------------------- Mempool High Value Transaction Monitoring --------------------- 118 | # High-value threshold defined in Wei. 119 | HIGH_VALUE_THRESHOLD=1000000000000000000 120 | -------------------------------------------------------------------------------- /tests/test_abiregistry.py: -------------------------------------------------------------------------------- 1 | # LICENSE: MIT // github.com/John0n1/ON1Builder 2 | 3 | import pytest 4 | from pathlib import Path 5 | from unittest.mock import patch, AsyncMock 6 | from python.abiregistry import ABIRegistry 7 | 8 | @pytest.fixture 9 | def abi_registry(): 10 | return ABIRegistry() 11 | 12 | @pytest.mark.asyncio 13 | async def test_initialize(abi_registry): 14 | with patch('python.abiregistry.ABIRegistry._load_all_abis', new_callable=AsyncMock) as mock_load_all_abis: 15 | await abi_registry.initialize() 16 | mock_load_all_abis.assert_called_once() 17 | assert abi_registry._initialized is True 18 | 19 | @pytest.mark.asyncio 20 | async def test_load_and_validate_abi(abi_registry): 21 | abi_type = 'erc20' 22 | abi_path = Path('tests/abi/erc20_abi.json') 23 | critical_abis = {'erc20'} 24 | 25 | with patch('python.abiregistry.ABIRegistry._load_abi_from_path', new_callable=AsyncMock) as mock_load_abi_from_path: 26 | mock_load_abi_from_path.return_value = [{'name': 'transfer', 'type': 'function', 'inputs': []}] 27 | await abi_registry._load_and_validate_abi(abi_type, abi_path, critical_abis) 28 | assert abi_type in abi_registry.abis 29 | assert abi_registry.abis[abi_type] == [{'name': 'transfer', 'type': 'function', 'inputs': []}] 30 | 31 | @pytest.mark.asyncio 32 | async def test_load_abi_from_path(abi_registry): 33 | abi_path = Path('tests/abi/erc20_abi.json') 34 | abi_type = 'erc20' 35 | 36 | with patch('aiofiles.open', new_callable=AsyncMock) as mock_open: 37 | mock_open.return_value.__aenter__.return_value.read.return_value = '[{"name": "transfer", "type": "function", "inputs": []}]' 38 | abi = await abi_registry._load_abi_from_path(abi_path, abi_type) 39 | assert abi == [{'name': 'transfer', 'type': 'function', 'inputs': []}] 40 | 41 | def test_validate_abi(abi_registry): 42 | abi = [{'name': 'transfer', 'type': 'function', 'inputs': []}] 43 | abi_type = 'erc20' 44 | assert abi_registry._validate_abi(abi, abi_type) is True 45 | 46 | def test_extract_signatures(abi_registry): 47 | abi = [{'name': 'transfer', 'type': 'function', 'inputs': []}] 48 | abi_type = 'erc20' 49 | abi_registry._extract_signatures(abi, abi_type) 50 | assert abi_registry.signatures[abi_type] == {'transfer': 'transfer()'} 51 | assert 'a9059cbb' in abi_registry.method_selectors[abi_type] 52 | 53 | def test_get_abi(abi_registry): 54 | abi_registry.abis['erc20'] = [{'name': 'transfer', 'type': 'function', 'inputs': []}] 55 | assert abi_registry.get_abi('erc20') == [{'name': 'transfer', 'type': 'function', 'inputs': []}] 56 | 57 | def test_get_method_selector(abi_registry): 58 | abi_registry.method_selectors['erc20'] = {'a9059cbb': 'transfer'} 59 | assert abi_registry.get_method_selector('a9059cbb') == 'transfer' 60 | 61 | def test_get_function_signature(abi_registry): 62 | abi_registry.signatures['erc20'] = {'transfer': 'transfer()'} 63 | assert abi_registry.get_function_signature('erc20', 'transfer') == 'transfer()' 64 | 65 | @pytest.mark.asyncio 66 | async def test_update_abi(abi_registry): 67 | abi_type = 'erc20' 68 | new_abi = [{'name': 'transfer', 'type': 'function', 'inputs': []}] 69 | await abi_registry.update_abi(abi_type, new_abi) 70 | assert abi_registry.abis[abi_type] == new_abi 71 | assert abi_registry.signatures[abi_type] == {'transfer': 'transfer()'} 72 | assert 'a9059cbb' in abi_registry.method_selectors[abi_type] 73 | -------------------------------------------------------------------------------- /tests/test_apiconfig.py: -------------------------------------------------------------------------------- 1 | # LICENSE: MIT // github.com/John0n1/ON1Builder 2 | 3 | import pytest 4 | from unittest.mock import AsyncMock, patch 5 | from python.apiconfig import APIConfig 6 | from python.configuration import Configuration 7 | 8 | @pytest.fixture 9 | def configuration(): 10 | config = Configuration() 11 | config.WALLET_KEY = "test_wallet_key" 12 | config.BASE_PATH = "test_base_path" 13 | config.HTTP_ENDPOINT = "http://localhost:8545" 14 | config.WEBSOCKET_ENDPOINT = "ws://localhost:8546" 15 | config.IPC_ENDPOINT = "/path/to/geth.ipc" 16 | return config 17 | 18 | @pytest.fixture 19 | def apiconfig(configuration): 20 | return APIConfig(configuration) 21 | 22 | @pytest.mark.asyncio 23 | async def test_initialize(apiconfig): 24 | with patch.object(apiconfig, 'initialize', new_callable=AsyncMock) as mock_initialize: 25 | await apiconfig.initialize() 26 | mock_initialize.assert_called_once() 27 | 28 | @pytest.mark.asyncio 29 | async def test_get_token_symbol(apiconfig): 30 | address = "0xTokenAddress" 31 | with patch.object(apiconfig, 'get_token_symbol', new_callable=AsyncMock) as mock_get_token_symbol: 32 | await apiconfig.get_token_symbol(address) 33 | mock_get_token_symbol.assert_called_once_with(address) 34 | 35 | @pytest.mark.asyncio 36 | async def test_get_token_address(apiconfig): 37 | symbol = "TEST" 38 | with patch.object(apiconfig, 'get_token_address', new_callable=AsyncMock) as mock_get_token_address: 39 | await apiconfig.get_token_address(symbol) 40 | mock_get_token_address.assert_called_once_with(symbol) 41 | 42 | @pytest.mark.asyncio 43 | async def test_get_token_metadata(apiconfig): 44 | token = "TEST" 45 | with patch.object(apiconfig, 'get_token_metadata', new_callable=AsyncMock) as mock_get_token_metadata: 46 | await apiconfig.get_token_metadata(token) 47 | mock_get_token_metadata.assert_called_once_with(token) 48 | 49 | @pytest.mark.asyncio 50 | async def test_get_real_time_price(apiconfig): 51 | token = "TEST" 52 | vs_currency = "eth" 53 | with patch.object(apiconfig, 'get_real_time_price', new_callable=AsyncMock) as mock_get_real_time_price: 54 | await apiconfig.get_real_time_price(token, vs_currency) 55 | mock_get_real_time_price.assert_called_once_with(token, vs_currency) 56 | 57 | @pytest.mark.asyncio 58 | async def test_make_request(apiconfig): 59 | provider_name = "coingecko" 60 | url = "https://api.coingecko.com/api/v3/simple/price" 61 | params = {"ids": "bitcoin", "vs_currencies": "usd"} 62 | headers = {"x-cg-pro-api-key": "test_api_key"} 63 | with patch.object(apiconfig, 'make_request', new_callable=AsyncMock) as mock_make_request: 64 | await apiconfig.make_request(provider_name, url, params, headers) 65 | mock_make_request.assert_called_once_with(provider_name, url, params, headers) 66 | 67 | @pytest.mark.asyncio 68 | async def test_fetch_historical_prices(apiconfig): 69 | token = "TEST" 70 | days = 30 71 | with patch.object(apiconfig, 'fetch_historical_prices', new_callable=AsyncMock) as mock_fetch_historical_prices: 72 | await apiconfig.fetch_historical_prices(token, days) 73 | mock_fetch_historical_prices.assert_called_once_with(token, days) 74 | 75 | @pytest.mark.asyncio 76 | async def test_get_token_volume(apiconfig): 77 | token = "TEST" 78 | with patch.object(apiconfig, 'get_token_volume', new_callable=AsyncMock) as mock_get_token_volume: 79 | await apiconfig.get_token_volume(token) 80 | mock_get_token_volume.assert_called_once_with(token) 81 | 82 | @pytest.mark.asyncio 83 | async def test_get_token_price_data(apiconfig): 84 | token_symbol = "TEST" 85 | data_type = "current" 86 | timeframe = 1 87 | vs_currency = "eth" 88 | with patch.object(apiconfig, 'get_token_price_data', new_callable=AsyncMock) as mock_get_token_price_data: 89 | await apiconfig.get_token_price_data(token_symbol, data_type, timeframe, vs_currency) 90 | mock_get_token_price_data.assert_called_once_with(token_symbol, data_type, timeframe, vs_currency) 91 | 92 | @pytest.mark.asyncio 93 | async def test_update_training_data(apiconfig): 94 | with patch.object(apiconfig, 'update_training_data', new_callable=AsyncMock) as mock_update_training_data: 95 | await apiconfig.update_training_data() 96 | mock_update_training_data.assert_called_once() 97 | 98 | @pytest.mark.asyncio 99 | async def test_train_price_model(apiconfig): 100 | with patch.object(apiconfig, 'train_price_model', new_callable=AsyncMock) as mock_train_price_model: 101 | await apiconfig.train_price_model() 102 | mock_train_price_model.assert_called_once() 103 | 104 | @pytest.mark.asyncio 105 | async def test_predict_price(apiconfig): 106 | token = "TEST" 107 | with patch.object(apiconfig, 'predict_price', new_callable=AsyncMock) as mock_predict_price: 108 | await apiconfig.predict_price(token) 109 | mock_predict_price.assert_called_once_with(token) 110 | 111 | @pytest.mark.asyncio 112 | async def test_get_dynamic_gas_price(apiconfig): 113 | with patch.object(apiconfig, 'get_dynamic_gas_price', new_callable=AsyncMock) as mock_get_dynamic_gas_price: 114 | await apiconfig.get_dynamic_gas_price() 115 | mock_get_dynamic_gas_price.assert_called_once() 116 | -------------------------------------------------------------------------------- /tests/test_configuration.py: -------------------------------------------------------------------------------- 1 | # LICENSE: MIT // github.com/John0n1/ON1Builder 2 | 3 | import pytest 4 | from unittest.mock import AsyncMock, patch 5 | from python.configuration import Configuration 6 | 7 | @pytest.fixture 8 | def configuration(): 9 | config = Configuration() 10 | config.WALLET_KEY = "test_wallet_key" 11 | config.BASE_PATH = "test_base_path" 12 | config.HTTP_ENDPOINT = "http://localhost:8545" 13 | config.WEBSOCKET_ENDPOINT = "ws://localhost:8546" 14 | config.IPC_ENDPOINT = "/path/to/geth.ipc" 15 | return config 16 | 17 | @pytest.fixture 18 | def configuration_instance(configuration): 19 | return Configuration(configuration) 20 | 21 | @pytest.mark.asyncio 22 | async def test_load_env(configuration_instance): 23 | with patch('python.configuration.dotenv.load_dotenv') as mock_load_dotenv: 24 | configuration_instance._load_env() 25 | mock_load_dotenv.assert_called_once_with(dotenv_path=configuration_instance.env_path) 26 | 27 | @pytest.mark.asyncio 28 | async def test_validate_ethereum_address(configuration_instance): 29 | valid_address = "0x0000000000000000000000000000000000000000" 30 | invalid_address = "0xInvalidAddress" 31 | with pytest.raises(ValueError): 32 | configuration_instance._validate_ethereum_address(invalid_address, "TEST_VAR") 33 | assert configuration_instance._validate_ethereum_address(valid_address, "TEST_VAR") == valid_address 34 | 35 | @pytest.mark.asyncio 36 | async def test_get_env_str(configuration_instance): 37 | with patch('python.configuration.os.getenv', return_value="test_value") as mock_getenv: 38 | value = configuration_instance._get_env_str("TEST_VAR", "default_value", "Test Description") 39 | assert value == "test_value" 40 | mock_getenv.assert_called_once_with("TEST_VAR", "default_value") 41 | 42 | @pytest.mark.asyncio 43 | async def test_get_env_int(configuration_instance): 44 | with patch('python.configuration.os.getenv', return_value="123") as mock_getenv: 45 | value = configuration_instance._get_env_int("TEST_VAR", 456, "Test Description") 46 | assert value == 123 47 | mock_getenv.assert_called_once_with("TEST_VAR") 48 | 49 | @pytest.mark.asyncio 50 | async def test_get_env_float(configuration_instance): 51 | with patch('python.configuration.os.getenv', return_value="123.45") as mock_getenv: 52 | value = configuration_instance._get_env_float("TEST_VAR", 456.78, "Test Description") 53 | assert value == 123.45 54 | mock_getenv.assert_called_once_with("TEST_VAR") 55 | 56 | @pytest.mark.asyncio 57 | async def test_resolve_path(configuration_instance): 58 | with patch('python.configuration.os.getenv', return_value="test_path") as mock_getenv, \ 59 | patch('python.configuration.Path.exists', return_value=True) as mock_exists: 60 | path = configuration_instance._resolve_path("TEST_PATH_VAR", "Test Description") 61 | assert path == configuration_instance.BASE_PATH / "test_path" 62 | mock_getenv.assert_called_once_with("TEST_PATH_VAR", None) 63 | mock_exists.assert_called_once() 64 | 65 | @pytest.mark.asyncio 66 | async def test_load_json_safe(configuration_instance): 67 | test_path = configuration_instance.BASE_PATH / "test.json" 68 | test_data = {"key": "value"} 69 | with patch('python.configuration.aiofiles.open', new_callable=AsyncMock) as mock_open, \ 70 | patch('python.configuration.json.loads', return_value=test_data) as mock_json_loads: 71 | mock_open.return_value.__aenter__.return_value.read.return_value = '{"key": "value"}' 72 | data = await configuration_instance._load_json_safe(test_path, "Test Description") 73 | assert data == test_data 74 | mock_open.assert_called_once_with(test_path, 'r') 75 | mock_json_loads.assert_called_once_with('{"key": "value"}') 76 | 77 | @pytest.mark.asyncio 78 | async def test_get_token_addresses(configuration_instance): 79 | test_data = {"0xTokenAddress": "TokenSymbol"} 80 | with patch.object(configuration_instance, '_load_json_safe', return_value=test_data) as mock_load_json_safe: 81 | addresses = await configuration_instance.get_token_addresses() 82 | assert addresses == ["0xTokenAddress"] 83 | mock_load_json_safe.assert_called_once_with(configuration_instance.TOKEN_ADDRESSES, "monitored tokens") 84 | 85 | @pytest.mark.asyncio 86 | async def test_get_token_symbols(configuration_instance): 87 | test_data = {"0xTokenAddress": "TokenSymbol"} 88 | with patch.object(configuration_instance, '_load_json_safe', return_value=test_data) as mock_load_json_safe: 89 | symbols = await configuration_instance.get_token_symbols() 90 | assert symbols == test_data 91 | mock_load_json_safe.assert_called_once_with(configuration_instance.TOKEN_SYMBOLS, "token symbols") 92 | 93 | @pytest.mark.asyncio 94 | async def test_get_erc20_signatures(configuration_instance): 95 | test_data = {"function_name": "selector"} 96 | with patch.object(configuration_instance, '_load_json_safe', return_value=test_data) as mock_load_json_safe: 97 | signatures = await configuration_instance.get_erc20_signatures() 98 | assert signatures == test_data 99 | mock_load_json_safe.assert_called_once_with(configuration_instance.ERC20_SIGNATURES, "ERC20 function signatures") 100 | 101 | @pytest.mark.asyncio 102 | async def test_get_config_value(configuration_instance): 103 | configuration_instance.TEST_VAR = "test_value" 104 | value = configuration_instance.get_config_value("TEST_VAR", "default_value") 105 | assert value == "test_value" 106 | 107 | @pytest.mark.asyncio 108 | async def test_get_all_config_values(configuration_instance): 109 | config_values = configuration_instance.get_all_config_values() 110 | assert isinstance(config_values, dict) 111 | assert "WALLET_KEY" in config_values 112 | 113 | @pytest.mark.asyncio 114 | async def test_load_abi_from_path(configuration_instance): 115 | test_path = configuration_instance.BASE_PATH / "test_abi.json" 116 | test_data = [{"name": "function", "type": "function"}] 117 | with patch('python.configuration.aiofiles.open', new_callable=AsyncMock) as mock_open, \ 118 | patch('python.configuration.json.loads', return_value=test_data) as mock_json_loads: 119 | mock_open.return_value.__aenter__.return_value.read.return_value = '[{"name": "function", "type": "function"}]' 120 | abi = await configuration_instance.load_abi_from_path(test_path) 121 | assert abi == test_data 122 | mock_open.assert_called_once_with(test_path, 'r') 123 | mock_json_loads.assert_called_once_with('[{"name": "function", "type": "function"}]') 124 | 125 | @pytest.mark.asyncio 126 | async def test_load(configuration_instance): 127 | with patch.object(configuration_instance, '_create_required_directories', new_callable=AsyncMock) as mock_create_dirs, \ 128 | patch.object(configuration_instance, '_load_critical_abis', new_callable=AsyncMock) as mock_load_abis, \ 129 | patch.object(configuration_instance, '_validate_api_keys', new_callable=AsyncMock) as mock_validate_keys, \ 130 | patch.object(configuration_instance, '_validate_addresses', new_callable=AsyncMock) as mock_validate_addresses: 131 | await configuration_instance.load() 132 | mock_create_dirs.assert_called_once() 133 | mock_load_abis.assert_called_once() 134 | mock_validate_keys.assert_called_once() 135 | mock_validate_addresses.assert_called_once() 136 | 137 | @pytest.mark.asyncio 138 | async def test_create_required_directories(configuration_instance): 139 | with patch('python.configuration.os.makedirs') as mock_makedirs: 140 | configuration_instance._create_required_directories() 141 | assert mock_makedirs.call_count == 3 142 | 143 | @pytest.mark.asyncio 144 | async def test_load_critical_abis(configuration_instance): 145 | with patch.object(configuration_instance, '_resolve_path', return_value="test_path") as mock_resolve_path: 146 | await configuration_instance._load_critical_abis() 147 | assert mock_resolve_path.call_count == 6 148 | 149 | @pytest.mark.asyncio 150 | async def test_validate_api_keys(configuration_instance): 151 | with patch('python.configuration.aiohttp.ClientSession') as mock_session: 152 | mock_session.return_value.__aenter__.return_value.post.return_value.__aenter__.return_value.json.return_value = {"result": "success"} 153 | await configuration_instance._validate_api_keys() 154 | assert mock_session.called 155 | 156 | @pytest.mark.asyncio 157 | async def test_validate_addresses(configuration_instance): 158 | with patch.object(configuration_instance, '_validate_ethereum_address', return_value="0xValidAddress") as mock_validate_address: 159 | configuration_instance.WALLET_ADDRESS = "0xValidAddress" 160 | configuration_instance._validate_addresses() 161 | mock_validate_address.assert_called_once_with("0xValidAddress", "WALLET_ADDRESS") 162 | -------------------------------------------------------------------------------- /tests/test_init.py: -------------------------------------------------------------------------------- 1 | # LICENSE: MIT // github.com/John0n1/ON1Builder 2 | 3 | import unittest 4 | 5 | class TestInit(unittest.TestCase): 6 | def test_import(self): 7 | try: 8 | import python.pyutils 9 | except ImportError: 10 | self.fail("Failed to import python.pyutils") 11 | 12 | if __name__ == '__main__': 13 | unittest.main() 14 | -------------------------------------------------------------------------------- /tests/test_loggingconfig.py: -------------------------------------------------------------------------------- 1 | # LICENSE: MIT // github.com/John0n1/ON1Builder 2 | 3 | import pytest 4 | from unittest.mock import patch 5 | import logging 6 | from python.loggingconfig import setup_logging 7 | 8 | @pytest.fixture 9 | def logger(): 10 | return setup_logging("TestLogger", level=logging.DEBUG) 11 | 12 | def test_setup_logging(logger): 13 | assert logger.name == "TestLogger" 14 | assert logger.level == logging.DEBUG 15 | 16 | def test_logging_output(logger, capsys): 17 | logger.debug("Debug message") 18 | logger.info("Info message") 19 | logger.warning("Warning message") 20 | logger.error("Error message") 21 | logger.critical("Critical message") 22 | 23 | captured = capsys.readouterr() 24 | assert "Debug message" in captured.out 25 | assert "Info message" in captured.out 26 | assert "Warning message" in captured.out 27 | assert "Error message" in captured.out 28 | assert "Critical message" in captured.out 29 | 30 | def test_spinner_task(): 31 | with patch('python.loggingconfig.sys.stdout.write') as mock_write, \ 32 | patch('python.loggingconfig.sys.stdout.flush') as mock_flush, \ 33 | patch('python.loggingconfig.time.sleep', side_effect=Exception("Stop")): 34 | stop_event = patch('threading.Event').start() 35 | stop_event.is_set.side_effect = [False, False, True] 36 | try: 37 | setup_logging.spinner_task("Loading", stop_event) 38 | except Exception as e: 39 | assert str(e) == "Stop" 40 | mock_write.assert_called() 41 | mock_flush.assert_called() 42 | -------------------------------------------------------------------------------- /tests/test_main.py: -------------------------------------------------------------------------------- 1 | # LICENSE: MIT // github.com/John0n1/ON1Builder 2 | 3 | import pytest 4 | from unittest.mock import AsyncMock, patch 5 | from python.main import main, run_bot 6 | 7 | @pytest.mark.asyncio 8 | async def test_main(): 9 | with patch('src.main.run_bot', new_callable=AsyncMock) as mock_run_bot: 10 | await main() 11 | mock_run_bot.assert_called_once() 12 | 13 | @pytest.mark.asyncio 14 | async def test_run_bot(): 15 | with patch('src.maincore.MainCore.initialize', new_callable=AsyncMock) as mock_initialize, \ 16 | patch('src.maincore.MainCore.run', new_callable=AsyncMock) as mock_run: 17 | await run_bot() 18 | mock_initialize.assert_called_once() 19 | mock_run.assert_called_once() 20 | -------------------------------------------------------------------------------- /tests/test_maincore.py: -------------------------------------------------------------------------------- 1 | # LICENSE: MIT // github.com/John0n1/ON1Builder 2 | 3 | import pytest 4 | from unittest.mock import AsyncMock, patch 5 | from python.maincore import MainCore 6 | from python.configuration import Configuration 7 | 8 | @pytest.fixture 9 | def configuration(): 10 | config = Configuration() 11 | config.WALLET_KEY = "test_wallet_key" 12 | config.BASE_PATH = "test_base_path" 13 | config.HTTP_ENDPOINT = "http://localhost:8545" 14 | config.WEBSOCKET_ENDPOINT = "ws://localhost:8546" 15 | config.IPC_ENDPOINT = "/path/to/geth.ipc" 16 | return config 17 | 18 | @pytest.fixture 19 | def maincore(configuration): 20 | return MainCore(configuration) 21 | 22 | @pytest.mark.asyncio 23 | async def test_initialize_components(maincore): 24 | with patch.object(maincore, '_load_configuration', new_callable=AsyncMock) as mock_load_config, \ 25 | patch.object(maincore, '_initialize_web3', new_callable=AsyncMock) as mock_init_web3, \ 26 | patch.object(maincore, '_check_account_balance', new_callable=AsyncMock) as mock_check_balance, \ 27 | patch('abiregistry.ABIRegistry.initialize', new_callable=AsyncMock) as mock_abi_init, \ 28 | patch('apiconfig.APIConfig.initialize', new_callable=AsyncMock) as mock_api_config_init, \ 29 | patch('noncecore.NonceCore.initialize', new_callable=AsyncMock) as mock_nonce_init, \ 30 | patch('safetynet.SafetyNet.initialize', new_callable=AsyncMock) as mock_safetynet_init, \ 31 | patch('transactioncore.TransactionCore.initialize', new_callable=AsyncMock) as mock_txcore_init, \ 32 | patch('marketmonitor.MarketMonitor.initialize', new_callable=AsyncMock) as mock_marketmonitor_init, \ 33 | patch('mempoolmonitor.MempoolMonitor.initialize', new_callable=AsyncMock) as mock_mempoolmonitor_init, \ 34 | patch('strategynet.StrategyNet.initialize', new_callable=AsyncMock) as mock_strategynet_init: 35 | 36 | await maincore._initialize_components() 37 | 38 | mock_load_config.assert_called_once() 39 | mock_init_web3.assert_called_once() 40 | mock_check_balance.assert_called_once() 41 | mock_abi_init.assert_called_once() 42 | mock_api_config_init.assert_called_once() 43 | mock_nonce_init.assert_called_once() 44 | mock_safetynet_init.assert_called_once() 45 | mock_txcore_init.assert_called_once() 46 | mock_marketmonitor_init.assert_called_once() 47 | mock_mempoolmonitor_init.assert_called_once() 48 | mock_strategynet_init.assert_called_once() 49 | 50 | @pytest.mark.asyncio 51 | async def test_initialize(maincore): 52 | with patch.object(maincore, '_initialize_components', new_callable=AsyncMock) as mock_init_components: 53 | await maincore.initialize() 54 | mock_init_components.assert_called_once() 55 | 56 | @pytest.mark.asyncio 57 | async def test_run(maincore): 58 | with patch.object(maincore.components['mempoolmonitor'], 'start_monitoring', new_callable=AsyncMock) as mock_start_monitoring, \ 59 | patch.object(maincore, '_process_profitable_transactions', new_callable=AsyncMock) as mock_process_tx, \ 60 | patch.object(maincore, '_monitor_memory', new_callable=AsyncMock) as mock_monitor_memory, \ 61 | patch.object(maincore, '_check_component_health', new_callable=AsyncMock) as mock_check_health: 62 | 63 | await maincore.run() 64 | 65 | mock_start_monitoring.assert_called_once() 66 | mock_process_tx.assert_called_once() 67 | mock_monitor_memory.assert_called_once() 68 | mock_check_health.assert_called_once() 69 | 70 | @pytest.mark.asyncio 71 | async def test_stop(maincore): 72 | with patch.object(maincore, '_stop_component', new_callable=AsyncMock) as mock_stop_component, \ 73 | patch.object(maincore.web3.provider, 'disconnect', new_callable=AsyncMock) as mock_disconnect: 74 | 75 | await maincore.stop() 76 | 77 | mock_stop_component.assert_called() 78 | mock_disconnect.assert_called_once() 79 | 80 | @pytest.mark.asyncio 81 | async def test_emergency_shutdown(maincore): 82 | with patch('asyncio.all_tasks', return_value=[]), \ 83 | patch.object(maincore.web3.provider, 'disconnect', new_callable=AsyncMock) as mock_disconnect: 84 | 85 | await maincore.emergency_shutdown() 86 | 87 | mock_disconnect.assert_called_once() 88 | -------------------------------------------------------------------------------- /tests/test_marketmonitor.py: -------------------------------------------------------------------------------- 1 | # LICENSE: MIT // github.com/John0n1/ON1Builder 2 | 3 | import pytest 4 | from unittest.mock import AsyncMock, patch 5 | from python.marketmonitor import MarketMonitor 6 | from python.configuration import Configuration 7 | 8 | @pytest.fixture 9 | def configuration(): 10 | config = Configuration() 11 | config.WALLET_KEY = "test_wallet_key" 12 | config.BASE_PATH = "test_base_path" 13 | config.HTTP_ENDPOINT = "http://localhost:8545" 14 | config.WEBSOCKET_ENDPOINT = "ws://localhost:8546" 15 | config.IPC_ENDPOINT = "/path/to/geth.ipc" 16 | return config 17 | 18 | @pytest.fixture 19 | def marketmonitor(configuration): 20 | return MarketMonitor(configuration) 21 | 22 | @pytest.mark.asyncio 23 | async def test_initialize(marketmonitor): 24 | with patch.object(marketmonitor, 'initialize', new_callable=AsyncMock) as mock_initialize: 25 | await marketmonitor.initialize() 26 | mock_initialize.assert_called_once() 27 | 28 | @pytest.mark.asyncio 29 | async def test_schedule_updates(marketmonitor): 30 | with patch.object(marketmonitor, 'schedule_updates', new_callable=AsyncMock) as mock_schedule_updates: 31 | await marketmonitor.schedule_updates() 32 | mock_schedule_updates.assert_called_once() 33 | 34 | @pytest.mark.asyncio 35 | async def test_check_market_conditions(marketmonitor): 36 | token_address = "0xTokenAddress" 37 | with patch.object(marketmonitor, 'check_market_conditions', new_callable=AsyncMock) as mock_check_market_conditions: 38 | await marketmonitor.check_market_conditions(token_address) 39 | mock_check_market_conditions.assert_called_once_with(token_address) 40 | 41 | @pytest.mark.asyncio 42 | async def test_predict_price_movement(marketmonitor): 43 | token_symbol = "TEST" 44 | with patch.object(marketmonitor, 'predict_price_movement', new_callable=AsyncMock) as mock_predict_price_movement: 45 | await marketmonitor.predict_price_movement(token_symbol) 46 | mock_predict_price_movement.assert_called_once_with(token_symbol) 47 | 48 | @pytest.mark.asyncio 49 | async def test_get_market_features(marketmonitor): 50 | token_symbol = "TEST" 51 | with patch.object(marketmonitor, '_get_market_features', new_callable=AsyncMock) as mock_get_market_features: 52 | await marketmonitor._get_market_features(token_symbol) 53 | mock_get_market_features.assert_called_once_with(token_symbol) 54 | 55 | @pytest.mark.asyncio 56 | async def test_get_trading_metrics(marketmonitor): 57 | token_symbol = "TEST" 58 | with patch.object(marketmonitor, '_get_trading_metrics', new_callable=AsyncMock) as mock_get_trading_metrics: 59 | await marketmonitor._get_trading_metrics(token_symbol) 60 | mock_get_trading_metrics.assert_called_once_with(token_symbol) 61 | 62 | @pytest.mark.asyncio 63 | async def test_get_avg_transaction_value(marketmonitor): 64 | token_symbol = "TEST" 65 | with patch.object(marketmonitor, '_get_avg_transaction_value', new_callable=AsyncMock) as mock_get_avg_transaction_value: 66 | await marketmonitor._get_avg_transaction_value(token_symbol) 67 | mock_get_avg_transaction_value.assert_called_once_with(token_symbol) 68 | 69 | @pytest.mark.asyncio 70 | async def test_get_transaction_count(marketmonitor): 71 | token_symbol = "TEST" 72 | with patch.object(marketmonitor, '_get_transaction_count', new_callable=AsyncMock) as mock_get_transaction_count: 73 | await marketmonitor._get_transaction_count(token_symbol) 74 | mock_get_transaction_count.assert_called_once_with(token_symbol) 75 | 76 | @pytest.mark.asyncio 77 | async def test_get_trading_pairs_count(marketmonitor): 78 | token_symbol = "TEST" 79 | with patch.object(marketmonitor, '_get_trading_pairs_count', new_callable=AsyncMock) as mock_get_trading_pairs_count: 80 | await marketmonitor._get_trading_pairs_count(token_symbol) 81 | mock_get_trading_pairs_count.assert_called_once_with(token_symbol) 82 | 83 | @pytest.mark.asyncio 84 | async def test_get_exchange_count(marketmonitor): 85 | token_symbol = "TEST" 86 | with patch.object(marketmonitor, '_get_exchange_count', new_callable=AsyncMock) as mock_get_exchange_count: 87 | await marketmonitor._get_exchange_count(token_symbol) 88 | mock_get_exchange_count.assert_called_once_with(token_symbol) 89 | 90 | @pytest.mark.asyncio 91 | async def test_get_buy_sell_ratio(marketmonitor): 92 | token_symbol = "TEST" 93 | with patch.object(marketmonitor, '_get_buy_sell_ratio', new_callable=AsyncMock) as mock_get_buy_sell_ratio: 94 | await marketmonitor._get_buy_sell_ratio(token_symbol) 95 | mock_get_buy_sell_ratio.assert_called_once_with(token_symbol) 96 | 97 | @pytest.mark.asyncio 98 | async def test_get_smart_money_flow(marketmonitor): 99 | token_symbol = "TEST" 100 | with patch.object(marketmonitor, '_get_smart_money_flow', new_callable=AsyncMock) as mock_get_smart_money_flow: 101 | await marketmonitor._get_smart_money_flow(token_symbol) 102 | mock_get_smart_money_flow.assert_called_once_with(token_symbol) 103 | 104 | @pytest.mark.asyncio 105 | async def test_get_price_data(marketmonitor): 106 | args = ("arg1", "arg2") 107 | kwargs = {"kwarg1": "value1"} 108 | with patch.object(marketmonitor, 'get_price_data', new_callable=AsyncMock) as mock_get_price_data: 109 | await marketmonitor.get_price_data(*args, **kwargs) 110 | mock_get_price_data.assert_called_once_with(*args, **kwargs) 111 | 112 | @pytest.mark.asyncio 113 | async def test_get_token_volume(marketmonitor): 114 | token_symbol = "TEST" 115 | with patch.object(marketmonitor, 'get_token_volume', new_callable=AsyncMock) as mock_get_token_volume: 116 | await marketmonitor.get_token_volume(token_symbol) 117 | mock_get_token_volume.assert_called_once_with(token_symbol) 118 | 119 | @pytest.mark.asyncio 120 | async def test_stop(marketmonitor): 121 | with patch.object(marketmonitor, 'stop', new_callable=AsyncMock) as mock_stop: 122 | await marketmonitor.stop() 123 | mock_stop.assert_called_once() 124 | 125 | @pytest.mark.asyncio 126 | async def test_get_token_price(marketmonitor): 127 | token_symbol = "TEST" 128 | data_type = "current" 129 | timeframe = 1 130 | vs_currency = "eth" 131 | with patch.object(marketmonitor, 'get_token_price', new_callable=AsyncMock) as mock_get_token_price: 132 | await marketmonitor.get_token_price(token_symbol, data_type, timeframe, vs_currency) 133 | mock_get_token_price.assert_called_once_with(token_symbol, data_type, timeframe, vs_currency) 134 | 135 | @pytest.mark.asyncio 136 | async def test_is_arbitrage_opportunity(marketmonitor): 137 | token_symbol = "TEST" 138 | with patch.object(marketmonitor, '_is_arbitrage_opportunity', new_callable=AsyncMock) as mock_is_arbitrage_opportunity: 139 | await marketmonitor._is_arbitrage_opportunity(token_symbol) 140 | mock_is_arbitrage_opportunity.assert_called_once_with(token_symbol) 141 | -------------------------------------------------------------------------------- /tests/test_mempoolmonitor.py: -------------------------------------------------------------------------------- 1 | # LICENSE: MIT // github.com/John0n1/ON1Builder 2 | 3 | import pytest 4 | from unittest.mock import AsyncMock, patch 5 | from python.mempoolmonitor import MempoolMonitor 6 | 7 | @pytest.fixture 8 | def mempool_monitor(): 9 | return MempoolMonitor() 10 | 11 | @pytest.mark.asyncio 12 | async def test_handle_new_transactions(mempool_monitor): 13 | transactions = ["tx1", "tx2", "tx3"] 14 | with patch.object(mempool_monitor, '_process_transaction', new_callable=AsyncMock) as mock_process_transaction: 15 | await mempool_monitor._handle_new_transactions(transactions) 16 | assert mock_process_transaction.call_count == len(transactions) 17 | 18 | @pytest.mark.asyncio 19 | async def test_queue_transaction(mempool_monitor): 20 | transaction = "tx1" 21 | with patch.object(mempool_monitor, '_process_transaction', new_callable=AsyncMock) as mock_process_transaction: 22 | await mempool_monitor._queue_transaction(transaction) 23 | mock_process_transaction.assert_called_once_with(transaction) 24 | 25 | @pytest.mark.asyncio 26 | async def test_monitor_memory(mempool_monitor): 27 | with patch('python.mempoolmonitor.psutil.virtual_memory') as mock_virtual_memory: 28 | mock_virtual_memory.return_value.percent = 50 29 | await mempool_monitor._monitor_memory() 30 | assert mock_virtual_memory.called 31 | 32 | @pytest.mark.asyncio 33 | async def test_get_dynamic_gas_price(mempool_monitor): 34 | with patch('python.mempoolmonitor.web3.eth.getBlock') as mock_get_block: 35 | mock_get_block.return_value = {'baseFeePerGas': 1000000000} 36 | gas_price = await mempool_monitor.get_dynamic_gas_price() 37 | assert gas_price == 1000000000 38 | -------------------------------------------------------------------------------- /tests/test_noncecore.py: -------------------------------------------------------------------------------- 1 | # LICENSE: MIT // github.com/John0n1/ON1Builder 2 | 3 | import pytest 4 | from unittest.mock import AsyncMock, patch 5 | from python.noncecore import NonceCore 6 | from python.configuration import Configuration 7 | from web3 import AsyncWeb3 8 | 9 | @pytest.fixture 10 | def configuration(): 11 | config = Configuration() 12 | config.WALLET_KEY = "test_wallet_key" 13 | config.BASE_PATH = "test_base_path" 14 | config.HTTP_ENDPOINT = "http://localhost:8545" 15 | config.WEBSOCKET_ENDPOINT = "ws://localhost:8546" 16 | config.IPC_ENDPOINT = "/path/to/geth.ipc" 17 | return config 18 | 19 | @pytest.fixture 20 | def noncecore(configuration): 21 | web3 = AsyncWeb3() 22 | address = "0xYourEthereumAddress" 23 | return NonceCore(web3, address, configuration) 24 | 25 | @pytest.mark.asyncio 26 | async def test_initialize(noncecore): 27 | with patch.object(noncecore, 'initialize', new_callable=AsyncMock) as mock_initialize: 28 | await noncecore.initialize() 29 | mock_initialize.assert_called_once() 30 | 31 | @pytest.mark.asyncio 32 | async def test_get_nonce(noncecore): 33 | with patch.object(noncecore, 'get_nonce', new_callable=AsyncMock) as mock_get_nonce: 34 | await noncecore.get_nonce() 35 | mock_get_nonce.assert_called_once() 36 | 37 | @pytest.mark.asyncio 38 | async def test_refresh_nonce(noncecore): 39 | with patch.object(noncecore, 'refresh_nonce', new_callable=AsyncMock) as mock_refresh_nonce: 40 | await noncecore.refresh_nonce() 41 | mock_refresh_nonce.assert_called_once() 42 | 43 | @pytest.mark.asyncio 44 | async def test_track_transaction(noncecore): 45 | tx_hash = "0xTransactionHash" 46 | nonce = 1 47 | with patch.object(noncecore, 'track_transaction', new_callable=AsyncMock) as mock_track_transaction: 48 | await noncecore.track_transaction(tx_hash, nonce) 49 | mock_track_transaction.assert_called_once_with(tx_hash, nonce) 50 | 51 | @pytest.mark.asyncio 52 | async def test_sync_nonce_with_chain(noncecore): 53 | with patch.object(noncecore, 'sync_nonce_with_chain', new_callable=AsyncMock) as mock_sync_nonce_with_chain: 54 | await noncecore.sync_nonce_with_chain() 55 | mock_sync_nonce_with_chain.assert_called_once() 56 | 57 | @pytest.mark.asyncio 58 | async def test_reset(noncecore): 59 | with patch.object(noncecore, 'reset', new_callable=AsyncMock) as mock_reset: 60 | await noncecore.reset() 61 | mock_reset.assert_called_once() 62 | 63 | @pytest.mark.asyncio 64 | async def test_stop(noncecore): 65 | with patch.object(noncecore, 'stop', new_callable=AsyncMock) as mock_stop: 66 | await noncecore.stop() 67 | mock_stop.assert_called_once() 68 | 69 | @pytest.mark.asyncio 70 | async def test_get_next_nonce(noncecore): 71 | with patch.object(noncecore, 'get_next_nonce', new_callable=AsyncMock) as mock_get_next_nonce: 72 | await noncecore.get_next_nonce() 73 | mock_get_next_nonce.assert_called_once() 74 | -------------------------------------------------------------------------------- /tests/test_safetynet.py: -------------------------------------------------------------------------------- 1 | # LICENSE: MIT // github.com/John0n1/ON1Builder 2 | 3 | import pytest 4 | from unittest.mock import AsyncMock, patch 5 | from python.safetynet import SafetyNet 6 | from python.configuration import Configuration 7 | from web3 import AsyncWeb3 8 | from eth_account import Account 9 | 10 | @pytest.fixture 11 | def configuration(): 12 | config = Configuration() 13 | config.SAFETYNET_CACHE_TTL = 60 14 | config.SAFETYNET_GAS_PRICE_TTL = 10 15 | config.MAX_GAS_PRICE_GWEI = 100 16 | config.MIN_PROFIT = 0.001 17 | return config 18 | 19 | @pytest.fixture 20 | def web3(): 21 | return AsyncWeb3() 22 | 23 | @pytest.fixture 24 | def account(): 25 | return Account.create() 26 | 27 | @pytest.fixture 28 | def safetynet(web3, configuration, account): 29 | return SafetyNet(web3, configuration=configuration, account=account) 30 | 31 | @pytest.mark.asyncio 32 | async def test_initialize(safetynet): 33 | with patch.object(safetynet.web3, 'is_connected', new_callable=AsyncMock) as mock_is_connected: 34 | mock_is_connected.return_value = True 35 | await safetynet.initialize() 36 | mock_is_connected.assert_called_once() 37 | 38 | @pytest.mark.asyncio 39 | async def test_get_balance(safetynet, account): 40 | with patch.object(safetynet.web3.eth, 'get_balance', new_callable=AsyncMock) as mock_get_balance: 41 | mock_get_balance.return_value = 1000000000000000000 # 1 ETH in wei 42 | balance = await safetynet.get_balance(account) 43 | assert balance == 1 44 | 45 | @pytest.mark.asyncio 46 | async def test_ensure_profit(safetynet): 47 | transaction_data = { 48 | 'output_token': '0xTokenAddress', 49 | 'amountOut': 100, 50 | 'amountIn': 1, 51 | 'gas_price': 50, 52 | 'gas_used': 21000 53 | } 54 | with patch.object(safetynet.apiconfig, 'get_real_time_price', new_callable=AsyncMock) as mock_get_real_time_price, \ 55 | patch.object(safetynet, '_calculate_gas_cost', return_value=0.001) as mock_calculate_gas_cost, \ 56 | patch.object(safetynet, 'adjust_slippage_tolerance', return_value=0.1) as mock_adjust_slippage_tolerance, \ 57 | patch.object(safetynet, '_calculate_profit', return_value=0.1) as mock_calculate_profit: 58 | mock_get_real_time_price.return_value = 0.01 59 | result = await safetynet.ensure_profit(transaction_data) 60 | assert result is True 61 | 62 | @pytest.mark.asyncio 63 | async def test_check_transaction_safety(safetynet): 64 | tx_data = { 65 | 'output_token': '0xTokenAddress', 66 | 'amountOut': 100, 67 | 'amountIn': 1, 68 | 'gas_price': 50, 69 | 'gas_used': 21000 70 | } 71 | with patch.object(safetynet, 'get_dynamic_gas_price', return_value=50) as mock_get_dynamic_gas_price, \ 72 | patch.object(safetynet.apiconfig, 'get_real_time_price', return_value=0.01) as mock_get_real_time_price, \ 73 | patch.object(safetynet, 'adjust_slippage_tolerance', return_value=0.1) as mock_adjust_slippage_tolerance, \ 74 | patch.object(safetynet, '_calculate_gas_cost', return_value=0.001) as mock_calculate_gas_cost, \ 75 | patch.object(safetynet, '_calculate_profit', return_value=0.1) as mock_calculate_profit, \ 76 | patch.object(safetynet, 'get_network_congestion', return_value=0.5) as mock_get_network_congestion: 77 | result, details = await safetynet.check_transaction_safety(tx_data) 78 | assert result is True 79 | assert details['is_safe'] is True 80 | assert details['gas_ok'] is True 81 | assert details['profit_ok'] is True 82 | assert details['congestion_ok'] is True 83 | -------------------------------------------------------------------------------- /tests/test_strategyexecutionerror.py: -------------------------------------------------------------------------------- 1 | # LICENSE: MIT // github.com/John0n1/ON1Builder 2 | 3 | import pytest 4 | from python.pyutils.strategyexecutionerror import StrategyExecutionError 5 | 6 | def test_strategy_execution_error(): 7 | error_message = "Test error message" 8 | error = StrategyExecutionError(error_message) 9 | 10 | assert str(error) == error_message 11 | assert isinstance(error, Exception) 12 | -------------------------------------------------------------------------------- /tests/test_transactioncore.py: -------------------------------------------------------------------------------- 1 | # LICENSE: MIT // github.com/John0n1/ON1Builder 2 | 3 | import pytest 4 | from unittest.mock import AsyncMock, patch 5 | from python.transactioncore import TransactionCore 6 | from python.configuration import Configuration 7 | 8 | @pytest.fixture 9 | def configuration(): 10 | config = Configuration() 11 | config.WALLET_KEY = "test_wallet_key" 12 | config.BASE_PATH = "test_base_path" 13 | config.HTTP_ENDPOINT = "http://localhost:8545" 14 | config.WEBSOCKET_ENDPOINT = "ws://localhost:8546" 15 | config.IPC_ENDPOINT = "/path/to/geth.ipc" 16 | return config 17 | 18 | @pytest.fixture 19 | def transactioncore(configuration): 20 | return TransactionCore(configuration) 21 | 22 | @pytest.mark.asyncio 23 | async def test_initialize(transactioncore): 24 | with patch.object(transactioncore, 'initialize', new_callable=AsyncMock) as mock_initialize: 25 | await transactioncore.initialize() 26 | mock_initialize.assert_called_once() 27 | 28 | @pytest.mark.asyncio 29 | async def test_build_transaction(transactioncore): 30 | function_call = AsyncMock() 31 | additional_params = {"param1": "value1"} 32 | with patch.object(transactioncore, 'build_transaction', new_callable=AsyncMock) as mock_build_transaction: 33 | await transactioncore.build_transaction(function_call, additional_params) 34 | mock_build_transaction.assert_called_once_with(function_call, additional_params) 35 | 36 | @pytest.mark.asyncio 37 | async def test_execute_transaction(transactioncore): 38 | tx = {"to": "0xAddress", "value": 1000} 39 | with patch.object(transactioncore, 'execute_transaction', new_callable=AsyncMock) as mock_execute_transaction: 40 | await transactioncore.execute_transaction(tx) 41 | mock_execute_transaction.assert_called_once_with(tx) 42 | 43 | @pytest.mark.asyncio 44 | async def test_handle_eth_transaction(transactioncore): 45 | target_tx = {"tx_hash": "0xHash", "value": 1000} 46 | with patch.object(transactioncore, 'handle_eth_transaction', new_callable=AsyncMock) as mock_handle_eth_transaction: 47 | await transactioncore.handle_eth_transaction(target_tx) 48 | mock_handle_eth_transaction.assert_called_once_with(target_tx) 49 | 50 | @pytest.mark.asyncio 51 | async def test_simulate_transaction(transactioncore): 52 | transaction = {"to": "0xAddress", "value": 1000} 53 | with patch.object(transactioncore, 'simulate_transaction', new_callable=AsyncMock) as mock_simulate_transaction: 54 | await transactioncore.simulate_transaction(transaction) 55 | mock_simulate_transaction.assert_called_once_with(transaction) 56 | 57 | @pytest.mark.asyncio 58 | async def test_prepare_flashloan_transaction(transactioncore): 59 | flashloan_asset = "0xAsset" 60 | flashloan_amount = 1000 61 | with patch.object(transactioncore, 'prepare_flashloan_transaction', new_callable=AsyncMock) as mock_prepare_flashloan_transaction: 62 | await transactioncore.prepare_flashloan_transaction(flashloan_asset, flashloan_amount) 63 | mock_prepare_flashloan_transaction.assert_called_once_with(flashloan_asset, flashloan_amount) 64 | 65 | @pytest.mark.asyncio 66 | async def test_send_bundle(transactioncore): 67 | transactions = [{"to": "0xAddress", "value": 1000}] 68 | with patch.object(transactioncore, 'send_bundle', new_callable=AsyncMock) as mock_send_bundle: 69 | await transactioncore.send_bundle(transactions) 70 | mock_send_bundle.assert_called_once_with(transactions) 71 | 72 | @pytest.mark.asyncio 73 | async def test_front_run(transactioncore): 74 | target_tx = {"tx_hash": "0xHash", "value": 1000} 75 | with patch.object(transactioncore, 'front_run', new_callable=AsyncMock) as mock_front_run: 76 | await transactioncore.front_run(target_tx) 77 | mock_front_run.assert_called_once_with(target_tx) 78 | 79 | @pytest.mark.asyncio 80 | async def test_back_run(transactioncore): 81 | target_tx = {"tx_hash": "0xHash", "value": 1000} 82 | with patch.object(transactioncore, 'back_run', new_callable=AsyncMock) as mock_back_run: 83 | await transactioncore.back_run(target_tx) 84 | mock_back_run.assert_called_once_with(target_tx) 85 | 86 | @pytest.mark.asyncio 87 | async def test_execute_sandwich_attack(transactioncore): 88 | target_tx = {"tx_hash": "0xHash", "value": 1000} 89 | strategy = "default" 90 | with patch.object(transactioncore, 'execute_sandwich_attack', new_callable=AsyncMock) as mock_execute_sandwich_attack: 91 | await transactioncore.execute_sandwich_attack(target_tx, strategy) 92 | mock_execute_sandwich_attack.assert_called_once_with(target_tx, strategy) 93 | 94 | @pytest.mark.asyncio 95 | async def test_cancel_transaction(transactioncore): 96 | nonce = 1 97 | with patch.object(transactioncore, 'cancel_transaction', new_callable=AsyncMock) as mock_cancel_transaction: 98 | await transactioncore.cancel_transaction(nonce) 99 | mock_cancel_transaction.assert_called_once_with(nonce) 100 | 101 | @pytest.mark.asyncio 102 | async def test_withdraw_eth(transactioncore): 103 | with patch.object(transactioncore, 'withdraw_eth', new_callable=AsyncMock) as mock_withdraw_eth: 104 | await transactioncore.withdraw_eth() 105 | mock_withdraw_eth.assert_called_once() 106 | 107 | @pytest.mark.asyncio 108 | async def test_transfer_profit_to_account(transactioncore): 109 | amount = 1000 110 | account = "0xAccount" 111 | with patch.object(transactioncore, 'transfer_profit_to_account', new_callable=AsyncMock) as mock_transfer_profit_to_account: 112 | await transactioncore.transfer_profit_to_account(amount, account) 113 | mock_transfer_profit_to_account.assert_called_once_with(amount, account) 114 | 115 | @pytest.mark.asyncio 116 | async def test_stop(transactioncore): 117 | with patch.object(transactioncore, 'stop', new_callable=AsyncMock) as mock_stop: 118 | await transactioncore.stop() 119 | mock_stop.assert_called_once() 120 | -------------------------------------------------------------------------------- /ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ON1Builder Bot Dashboard 6 | 7 | 161 | 162 | 163 |
164 |

ON1Builder Bot Dashboard

165 |
Loading status...
166 |
167 | 168 | 169 |
170 |
171 |

Performance Metrics

172 | 182 |
183 |
184 | 187 |
188 |
189 |
190 | Console 191 |
192 | 196 |
197 |
198 |
199 |
200 | 201 | 328 | 329 | 330 | -------------------------------------------------------------------------------- /utils/erc20_signatures.json: -------------------------------------------------------------------------------- 1 | { 2 | "transfer": "0xa9059cbb", 3 | "approve": "0x095ea7b3", 4 | "transferFrom": "0x23b872dd", 5 | "increaseAllowance": "0xd73dd623", 6 | "decreaseAllowance": "0x66188463", 7 | "name": "0x06fdde03", 8 | "symbol": "0x95d89b41", 9 | "decimals": "0x313ce567", 10 | "totalSupply": "0x18160ddd", 11 | "balanceOf": "0x70a08231", 12 | "allowance": "0xdd62ed3e", 13 | "transferAndCall": "0x0d4ee3d0", 14 | "approveAndCall": "0x7ff36ab5", 15 | "burn": "0x42966c68", 16 | "burnFrom": "0x79cc6790", 17 | "mint": "0x40c10f19", 18 | "mintAndCall": "0x4d2780f3", 19 | "pause": "0x8456cb59", 20 | "unpause": "0x3f4ba83a", 21 | "isPauser": "0x8f32d59b", 22 | "addPauser": "0x82dc1ec4", 23 | "renouncePauser": "0x6ef8d66d", 24 | "isMinter": "0x7d64bcb4", 25 | "addMinter": "0x9f868a3e", 26 | "renounceMinter": "0x98650275", 27 | "push": "0x8f8b2c06", 28 | "pull": "0x8a0d1e0b", 29 | "pushAndCall": "0x4d60d5f8", 30 | "pullAndCall": "0x3e8bf9a4", 31 | "permit": "0xd505accf", 32 | "forceTransfer": "0x9d4d8f6a", 33 | "forceBurn": "0x0c3f4b3e", 34 | "forceMint": "0x4d3e5c1e", 35 | "forceApprove": "0x3f515b6f", 36 | "forceIncreaseAllowance": "0x4d2780f3", 37 | "forceDecreaseAllowance": "0x66188463", 38 | "forceTransferFrom": "0x23b872dd", 39 | "forcePull": "0x3f4ba83a", 40 | "forcePullAndCall": "0x3f4ba83a", 41 | "forcePush": "0x3f4ba83a", 42 | "forcePushAndCall": "0x3f4ba83a", 43 | "forceMintAndCall": "0x3f4ba83a", 44 | "forceBurnFrom": "0x3f4ba83a", 45 | "forceTransferAndCall": "0x3f4ba83a", 46 | "forceApproveAndCall": "0x3f4ba83a", 47 | "forcePause": "0x3f4ba83a", 48 | "forceUnpause": "0x3f4ba83a", 49 | "forceAddPauser": "0x3f4ba83a", 50 | "forceRenouncePauser": "0x3f4ba83a", 51 | "forceAddMinter": "0x3f4ba83a", 52 | "forceRenounceMinter": "0x3f4ba83a", 53 | "forcePermit": "0x3f4ba83a", 54 | "safeTransfer": "0x42842e0e", 55 | "safeTransferFrom": "0xb88d4fde", 56 | "safeApprove": "0x095ea7b3", 57 | "safeIncreaseAllowance": "0x39509351", 58 | "safeDecreaseAllowance": "0xa457c2d7", 59 | "safeTransferAndCall": "0x0d4ee3d0", 60 | "safeApproveAndCall": "0x7ff36ab5", 61 | "safeBurn": "0x42966c68", 62 | "safeBurnFrom": "0x79cc6790", 63 | "safeMint": "0x40c10f19", 64 | "safeMintAndCall": "0x4d2780f3", 65 | "safePause": "0x8456cb59", 66 | "safeUnpause": "0x3f4ba83a", 67 | "safeIsPauser": "0x8f32d59b", 68 | "safeAddPauser": "0x82dc1ec4", 69 | "safeRenouncePauser": "0x6ef8d66d", 70 | "safeIsMinter": "0x7d64bcb4", 71 | "safeAddMinter": "0x9f868a3e", 72 | "safeRenounceMinter": "0x98650275", 73 | "safePush": "0x8f8b2c06", 74 | "safePull": "0x8a0d1e0b", 75 | "safePushAndCall": "0x4d60d5f8", 76 | "safePullAndCall": "0x3e8bf9a4", 77 | "safePermit": "0xd505accf", 78 | "safeForceTransfer": "0x9d4d8f6a", 79 | "safeForceBurn": "0x0c3f4b3e", 80 | "safeForceMint": "0x4d3e5c1e", 81 | "safeForceApprove": "0x3f515b6f", 82 | "safeForceIncreaseAllowance": "0x4d2780f3", 83 | "safeForceDecreaseAllowance": "0x66188463", 84 | "safeForceTransferFrom": "0x23b872dd", 85 | "safeForcePull": "0x3f4ba83a", 86 | "safeForcePullAndCall": "0x3f4ba83a", 87 | "safeForcePush": "0x3f4ba83a", 88 | "safeForcePushAndCall": "0x3f4ba83a", 89 | "safeForceMintAndCall": "0x3f4ba83a", 90 | "safeForceBurnFrom": "0x3f4ba83a", 91 | "safeForceTransferAndCall": "0x3f4ba83a", 92 | "safeForceApproveAndCall": "0x3f4ba83a", 93 | "safeForcePause": "0x3f4ba83a", 94 | "safeForceUnpause": "0x3f4ba83a", 95 | "safeForceAddPauser": "0x3f4ba83a", 96 | "safeForceRenouncePauser": "0x3f4ba83a", 97 | "safeForceAddMinter": "0x3f4ba83a", 98 | "safeForceRenounceMinter": "0x3f4ba83a", 99 | "safeForcePermit": "0x3f4ba83a" 100 | } 101 | -------------------------------------------------------------------------------- /utils/token_addresses.json: -------------------------------------------------------------------------------- 1 | { 2 | "WETH": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", 3 | "ETH": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", 4 | "WBTC": "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", 5 | "USDT": "0xdac17f958d2ee523a2206206994597c13d831ec7", 6 | "USDC": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", 7 | "DAI": "0x6b175474e89094c44da98b954eedeac495271d0f", 8 | "UNI": "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984", 9 | "LINK": "0x514910771af9ca656af840dff83e8264ecf986ca", 10 | "AAVE": "0x7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9", 11 | "MKR": "0x9f8f72aa9304c8b593d555f12ef6589cc3a579a2", 12 | "SNX": "0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f", 13 | "SUSHI": "0x6b3595068778dd592e39a122f4f5a5cf09c90fe2", 14 | "CRV": "0xd533a949740bb3306d119cc777fa900ba034cd52", 15 | "COMP": "0xc00e94cb662c3520282e6f5717214004a7f26888", 16 | "YFI": "0x0bc529c00c6401aef6d220be8c6ea1667f6ad93e", 17 | "BAL": "0xba100000625a3754423978a60c9317c58a424e3d", 18 | "1INCH": "0x111111111117dc0aa78b770fa6a738034120c302", 19 | "MATIC": "0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0", 20 | "SHIB": "0x95ad61b0a150d79219dcf64e1e6cc01f0b64c4ce", 21 | "APE": "0x4d224452801aced8b2f0aebe155379bb5d594381", 22 | "REN": "0x408e41876cccdc0f92210600ef50372656052a38" 23 | } 24 | -------------------------------------------------------------------------------- /utils/token_symbols.json: -------------------------------------------------------------------------------- 1 | { 2 | "ethereum": "WETH", 3 | "wrapped-bitcoin": "WBTC", 4 | "tether": "USDT", 5 | "usd-coin": "USDC", 6 | "dai": "DAI", 7 | "uniswap": "UNI", 8 | "chainlink": "LINK", 9 | "aave": "AAVE", 10 | "maker": "MKR", 11 | "synthetix-network-token": "SNX", 12 | "sushi": "SUSHI", 13 | "curve-dao-token": "CRV", 14 | "compound-governance-token": "COMP", 15 | "yearn-finance": "YFI", 16 | "balancer": "BAL", 17 | "1inch": "1INCH", 18 | "matic-network": "MATIC", 19 | "shiba-inu": "SHIB", 20 | "apecoin": "APE", 21 | "republic-protocol": "REN" 22 | } 23 | --------------------------------------------------------------------------------