├── .gitignore ├── LICENSE ├── README.md ├── analyse_logs.py ├── claim_staking_rewards.sh ├── contracts └── ServiceRegistryTokenUtility.json ├── images ├── docker.png ├── metamask_import_private_key.png ├── staking_fsm.svg └── trader_fsm_transitions.png ├── rank_traders.py ├── report.py ├── reset_staking.sh ├── run_service.sh ├── scripts ├── __init__.py ├── change_keys_json_password.py ├── check_python.py ├── choose_staking.py ├── claim_staking_rewards.py ├── erc20_balance.py ├── get_agent_bond.py ├── get_available_staking_slots.py ├── get_safe_owners.py ├── is_keys_json_password_valid.py ├── mech_events.py ├── service_hash.py ├── staking.py ├── swap_safe_owner.py └── utils.py ├── stop_service.sh ├── terminate_on_chain_service.sh └── trades.py /.gitignore: -------------------------------------------------------------------------------- 1 | .trader_runner* 2 | .operate* 3 | trader 4 | *.DS_Store 5 | __pycache__ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2023 Valory AG 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 🚨 WARNING: Repository will be deprecated soon 🚨 3 |

4 | 5 |

6 | This repository has been deprecated and further maintenance is not guaranteed moving forward. 7 |
8 | If you are using it for the first time, please use the new Quickstart. 9 |
10 | If you are already running agents using this quickstart, please use the Migration Script to migrate your agents to the new Quickstart. 11 |

12 | 13 | # trader-quickstart 14 | 15 | A quickstart for the trader agent for AI prediction markets on Gnosis at https://github.com/valory-xyz/trader 16 | 17 | ## Compatible Systems 18 | 19 | - Windows 10/11: WSL2 / Git BASH 20 | - Mac ARM / Intel 21 | - Linux 22 | - Raspberry Pi 4 23 | 24 | ## System Requirements 25 | 26 | Ensure your machine satisfies the requirements: 27 | 28 | - Python `==3.10` 29 | - [Poetry](https://python-poetry.org/docs/) `>=1.4.0` 30 | - [Docker Engine](https://docs.docker.com/engine/install/) `<25.0.0` 31 | - [Docker Compose](https://docs.docker.com/compose/install/) 32 | 33 | ## Resource Requirements 34 | 35 | - You need xDAI on Gnosis Chain in one of your wallets. 36 | - You need an RPC for your agent instance. We recommend [Quicknode RPC](https://www.quicknode.com/). 37 | - (From release v0.16.0 onwards) You will need a Subgraph API key that can be obtained at [The Graph](https://thegraph.com/studio/apikeys/). 38 | 39 | ## Run the Service 40 | 41 | ### For Non-Stakers 42 | 43 | Clone this repository locally and execute: 44 | 45 | ```bash 46 | chmod +x run_service.sh 47 | ./run_service.sh 48 | ``` 49 | 50 | Answer `1) No staking` when prompted: 51 | 52 | ```text 53 | Please, select your staking program preference 54 | ``` 55 | 56 | ### For Stakers 57 | 58 | > :warning: **Warning**
59 | > The code within this repository is provided without any warranties. It is important to note that the code has not been audited for potential security vulnerabilities. 60 | > Using this code could potentially lead to loss of funds, compromised data, or asset risk. 61 | > Exercise caution and use this code at your own risk. Please refer to the [LICENSE](./LICENSE) file for details about the terms and conditions. 62 | 63 | Each staking program has different OLAS requirements. The script will check that your owner address meets the minimum required OLAS on the Gnosis Chain. 64 | 65 | Clone this repository locally and execute: 66 | 67 | ```bash 68 | chmod +x run_service.sh 69 | ./run_service.sh 70 | ``` 71 | 72 | Select your preferred staking program when prompted: 73 | 74 | ```text 75 | Please, select your staking program preference 76 | ---------------------------------------------- 77 | 1) No staking 78 | Your Olas Predict agent will still actively participate in prediction 79 | markets, but it will not be staked within any staking program. 80 | 81 | 2) Quickstart Beta - Hobbyist 82 | The Quickstart Beta - Hobbyist staking contract offers 100 slots for 83 | operators running Olas Predict agents with the quickstart. It is designed as 84 | a step up from Coastal Staker Expeditions, requiring 100 OLAS for staking. 85 | The rewards are also more attractive than with Coastal Staker Expeditions. 86 | 87 | 3) Quickstart Beta - Expert 88 | The Quickstart Beta - Expert staking contract offers 20 slots for operators 89 | running Olas Predict agents with the quickstart. It is designed for 90 | professional agent operators, requiring 1000 OLAS for staking. The rewards 91 | are proportional to the Quickstart Beta - Hobbyist. 92 | 93 | ... 94 | ``` 95 | 96 | Find below a diagram of the possible status a service can be in the staking program: 97 | 98 | ![Staking FSM](images/staking_fsm.svg) 99 | 100 | Services can become staked by invoking the `stake()` contract method, where service parameters and deposit amounts are verified. Staked services can call the `checkpoint()` method at regular intervals, ensuring liveness checks and calculating staking rewards. In case a service remains inactive beyond the specified `maxAllowedInactivity` time, it faces eviction from the staking program, ceasing to accrue additional rewards. Staked or evicted services can be unstaked by calling the `unstake()` contract method. They can do so after `minStakingDuration` has passed or if no more staking rewards are available. 101 | 102 | __Notes__: 103 | 104 | - Staking is currently in a testing phase, so the number of trader agents that can be staked might be limited. 105 | - Services are evicted after accumulating 2 consecutive checkpoints without meeting the activity threshold. 106 | - Currently, the minimum staking time is approximately 3 days. In particular, a service cannot be unstaked during the minimum staking period. 107 | - Once a staking program is selected, you can reset your preference by stopping your agent by running ./stop_service.sh and then running the command 108 | 109 | ``` bash 110 | ./reset_staking.sh 111 | ``` 112 | 113 | Keep in mind that your service must stay for `minStakingDuration` in a staking program (typically 3 days) before you can change to a new program. 114 | 115 | ### Service is Running 116 | 117 | Once the command has completed, i.e. the service is running, you can see the live logs with: 118 | 119 | ```bash 120 | docker logs $(docker ps --filter "name=trader" --format "{{.Names}}" | grep "_abci" | head -n 1) --follow 121 | ``` 122 | 123 | To stop your agent, use: 124 | 125 | ```bash 126 | ./stop_service.sh 127 | ``` 128 | 129 | ### Claim accrued OLAS staking rewards 130 | 131 | If your service is staked, you can claim accrued OLAS staking rewards through the script 132 | 133 | ```bash 134 | ./claim_staking_rewards.sh 135 | ``` 136 | 137 | The accrued OLAS will be transferred to your service Safe without having to unstake your service. 138 | 139 | ### Backups 140 | 141 | Agent runners are recommended to create a [backup](https://github.com/valory-xyz/trader-quickstart#backup-and-recovery) of the relevant secret key material. 142 | 143 | ### Skip user prompts 144 | 145 | You can optionally pass `--attended=false` or export the environment variable `ATTENDED=false` to skip asking for inputs from the user. 146 | Note: In this case, if the service is staked, then it will not update the on-chain service, to avoid unstaking. If you choose a staking contract with different parameters (e.g., different bond), then you have to execute the attended mode of the script to update the on-chain service. 147 | 148 | ## Observe your agents 149 | 150 | 1. Use the `trades` command to display information about placed trades by a given address: 151 | 152 | ```bash 153 | cd trader; poetry run python ../trades.py --creator YOUR_SAFE_ADDRESS; cd .. 154 | ``` 155 | 156 | Or restrict the search to specific dates by defining the "from" and "to" dates: 157 | 158 | ```bash 159 | cd trader; poetry run python ../trades.py --creator YOUR_SAFE_ADDRESS --from-date 2023-08-15:03:50:00 --to-date 2023-08-20:13:45:00; cd .. 160 | ``` 161 | 162 | 2. Use the `report` command to display a summary of the service status: 163 | 164 | ```bash 165 | cd trader; poetry run python ../report.py; cd .. 166 | ``` 167 | 168 | 3. Use the `analyse_logs.py` script to investigate your agent's logs: 169 | 170 | ```bash 171 | cd trader; poetry run python ../analyse_logs.py --agent aea_0 --reset-db; cd .. 172 | ``` 173 | 174 | For example, inspect the state transitions using the following command: 175 | 176 | ```bash 177 | cd trader; poetry run python ../analyse_logs.py --agent aea_0 --fsm --reset-db; cd .. 178 | ``` 179 | 180 | This will output the different state transitions of your agent per period, for example: 181 | 182 | ![Trader FSM transitions](images/trader_fsm_transitions.png) 183 | 184 | For more options on the above command run: 185 | 186 | ```bash 187 | cd trader; poetry run autonomy analyse logs --help; cd .. 188 | ``` 189 | 190 | or take a look at the [command documentation](https://docs.autonolas.network/open-autonomy/advanced_reference/commands/autonomy_analyse/#autonomy-analyse-logs). 191 | 192 | ## Update between versions 193 | 194 | Simply pull the latest script: 195 | 196 | ```bash 197 | git pull origin 198 | ``` 199 | 200 | Remove the existing trader folder: 201 | 202 | ```bash 203 | rm -rf trader 204 | ``` 205 | 206 | Then continue above with "Run the script". 207 | 208 | ## Change the password of your key files 209 | 210 | > :warning: **Warning**
211 | > The code within this repository is provided without any warranties. It is important to note that the code has not been audited for potential security vulnerabilities. 212 | > 213 | > If you are updating the password for your key files, it is strongly advised to [create a backup](https://github.com/valory-xyz/trader-quickstart#backup-and-recovery) of the old configuration (located in the `./trader_runner` folder) before proceeding. This backup should be retained until you can verify that the changes are functioning as expected. For instance, run the service multiple times to ensure there are no issues with the new password before discarding the backup. 214 | 215 | If you have started you script specifying a password to protect your key files, you can change it by running the following command: 216 | 217 | ```bash 218 | cd trader; poetry run python ../scripts/change_keys_json_password.py ../.trader_runner --current_password --new_password ; cd .. 219 | ``` 220 | 221 | This will change the password in the following files: 222 | 223 | - `.trader_runner/keys.json` 224 | - `.trader_runner/operator_keys.json` 225 | - `.trader_runner/agent_pkey.txt` 226 | - `.trader_runner/operator_pkey.txt` 227 | 228 | If your key files are not encrypted, you must not use the `--current-password` argument. If you want to remove the password protection of your key files, 229 | you must not specify the `--new-password` argument. 230 | 231 | ## Advice for Mac users 232 | 233 | In Docker Desktop make sure that in `Settings -> Advanced` the following boxes are ticked 234 | 235 | ![Docker Desktop settings](images/docker.png) 236 | 237 | ## Advice for Windows users using Git BASH 238 | 239 | We provide some hints to have your Windows system ready to run the agent. The instructions below have been tested in Windows 11. 240 | 241 | Execute the following steps in a PowerShell terminal: 242 | 243 | 1. Install [Git](https://git-scm.com/download/win) and Git Bash: 244 | 245 | ```bash 246 | winget install --id Git.Git -e --source winget 247 | ``` 248 | 249 | 2. Install Python 3.10: 250 | 251 | ```bash 252 | winget install Python.Python.3.10 253 | ``` 254 | 255 | 3. Close and re-open the PowerShell terminal. 256 | 257 | 4. Install [Poetry](https://python-poetry.org/docs/): 258 | 259 | ```bash 260 | curl.exe -sSL https://install.python-poetry.org | python - 261 | ``` 262 | 263 | 5. Add Poetry to your user's path: 264 | 265 | ```bash 266 | $existingUserPath = (Get-Item -Path HKCU:\Environment).GetValue("PATH", $null, "DoNotExpandEnvironmentNames") 267 | 268 | $newUserPath = "$existingUserPath;$Env:APPDATA\Python\Scripts" 269 | 270 | [System.Environment]::SetEnvironmentVariable("Path", $newUserPath, "User") 271 | ``` 272 | 273 | 6. Install [Docker Desktop](https://www.docker.com/products/docker-desktop/): 274 | 275 | ```bash 276 | winget install -e --id Docker.DockerDesktop 277 | ``` 278 | 279 | 7. Log out of your Windows session and then log back in. 280 | 281 | 8. Open [Docker Desktop](https://www.docker.com/products/docker-desktop/) and leave it opened in the background. 282 | 283 | Now, open a Git Bash terminal and follow the instructions in the "[Run the script](#run-the-script)" section as well as the subsequent sections. You might need to install Microsoft Visual C++ 14.0 or greater. 284 | 285 | ## Advanced usage 286 | 287 | This chapter is for advanced users who want to further customize the trader agent's behaviour without changing the underlying trading logic. 288 | 289 | ##### Policy weights 290 | 291 | This script automatically sets some default weights to the agent's policy as a warm start. 292 | to help convergence and improve tool selection. 293 | These data were obtained after many days of running the service and are set 294 | [here](https://github.com/valory-xyz/trader-quickstart/blob/0f093ebbf0857b8484a017912c3992f00fbe1a29/run_service.sh#L133-L137). 295 | As a result, the current weights are always deleted and replaced by this strategy 296 | which is considered to boost the initial performance of the service. 297 | 298 | However, you may have found better performing policy weights and would like to remove this logic. 299 | It can easily be done, by removing this method call, 300 | [here](https://github.com/valory-xyz/trader-quickstart/blob/0f093ebbf0857b8484a017912c3992f00fbe1a29/run_service.sh#L698), 301 | in order to set your own custom warm start. 302 | Setting your own custom weights can be done by editing the corresponding files in `.trader_runner`. 303 | Moreover, you may store your current policy as a backup before editing those files, using the following set of commands: 304 | 305 | ```shell 306 | cp ".trader_runner/available_tools_store.json" ".trader_runner/available_tools_store_$(date +"%d-%m-%Y")".json 307 | cp ".trader_runner/policy_store.json" ".trader_runner/policy_store_$(date +"%d-%m-%Y")".json 308 | cp ".trader_runner/utilized_tools.json" ".trader_runner/utilized_tools_$(date +"%d-%m-%Y")".json 309 | ``` 310 | 311 | ##### Tool selection 312 | 313 | Sometimes, a mech tool might temporarily return invalid results. 314 | As a result, the service would end up performing mech calls without being able to use the response. 315 | Assuming that this tool has a large reward rate in the policy weights, 316 | the service might end up spending a considerable amount of xDAI before adjusting the tool's reward rate, 317 | without making any progress. 318 | If a tool is temporarily misbehaving, you could use an environment variable in order to exclude it. 319 | This environment variable is defined 320 | [here](https://github.com/valory-xyz/trader/blob/v0.8.0/packages/valory/services/trader/service.yaml#L109-L112) 321 | and can be overriden by setting it anywhere in the `run_service.sh` script with a new value, e.g.: 322 | 323 | ```shell 324 | IRRELEVANT_TOOLS=["some-misbehaving-tool", "openai-text-davinci-002", "openai-text-davinci-003", "openai-gpt-3.5-turbo", "openai-gpt-4", "stabilityai-stable-diffusion-v1-5", "stabilityai-stable-diffusion-xl-beta-v2-2-2", "stabilityai-stable-diffusion-512-v2-1", "stabilityai-stable-diffusion-768-v2-1"] 325 | ``` 326 | 327 | ##### Environment variables 328 | 329 | You may customize the agent's behaviour by setting these trader-specific environment variables. 330 | 331 | | Name | Type | Default Value | Description | 332 | | --- | --- | --- | --- | 333 | | `ON_CHAIN_SERVICE_ID` | `int` | `null` | The ID of the on-chain service. | 334 | | `OMEN_CREATORS` | `list` | `["0x89c5cc945dd550BcFfb72Fe42BfF002429F46Fec"]` | The addresses of the market creator(s) that the service will track. | 335 | | `OPENING_MARGIN` | `int` | `300` | The markets opening before this margin will not be fetched. | 336 | | `LANGUAGES` | `list` | `["en_US"]` | Filter questions by languages. | 337 | | `SAMPLE_BETS_CLOSING_DAYS` | `int` | `10` | Sample the bets that are closed within this number of days. | 338 | | `TRADING_STRATEGY` | `str` | `kelly_criterion_no_conf` | Trading strategy to use. | 339 | | `USE_FALLBACK_STRATEGY` | `bool` | `true` | Whether to use the fallback strategy. | 340 | | `BET_THRESHOLD` | `int` | `100000000000000000` | Threshold (wei) for placing a bet. A bet will only be placed if `potential_net_profit` - `BET_THRESHOLD` >= 0. | 341 | | `PROMPT_TEMPLATE` | `str` | `With the given question "@{question}" and the 'yes' option represented by '@{yes}' and the 'no' option represented by '@{no}', what are the respective probabilities of 'p_yes' and 'p_no' occurring?` | The prompt template to use for prompting the mech. | 342 | | `DUST_THRESHOLD` | `int` | `10000000000000` | Minimum amount (wei) below which a position's redeeming amount will be considered dust. | 343 | | `POLICY_EPSILON` | `float` | `0.1` | Epsilon value for the e-Greedy policy for the tool selection based on tool accuracy. | 344 | | `DISABLE_TRADING` | `bool` | `false` | Whether to disable trading. | 345 | | `STOP_TRADING_IF_STAKING_KPI_MET` | `bool` | `true` | Whether to stop trading if the staking KPI is met. | 346 | | `AGENT_BALANCE_THRESHOLD` | `int` | `10000000000000000` | Balance threshold (wei) below which the agent will stop trading and a refill will be required. | 347 | | `REFILL_CHECK_INTERVAL` | `int` | `10` | Interval in seconds to check the agent balance, when waiting for a refill. | 348 | | `FILE_HASH_TO_STRATEGIES_JSON` | `list` | `[["bafybeihufqu2ra7vud4h6g2nwahx7mvdido7ff6prwnib2tdlc4np7dw24",["bet_amount_per_threshold"]],["bafybeibxfp27rzrfnp7sxq62vwv32pdvrijxi7vzg7ihukkaka3bwzrgae",["kelly_criterion_no_conf"]]]` | A list of mapping from ipfs file hash to strategy names. | 349 | | `STRATEGIES_KWARGS` | `list` | `[["bet_kelly_fraction",1.0],["floor_balance",500000000000000000],["bet_amount_per_threshold",{"0.0":0,"0.1":0,"0.2":0,"0.3":0,"0.4":0,"0.5":0,"0.6":60000000000000000,"0.7":90000000000000000,"0.8":100000000000000000,"0.9":1000000000000000000,"1.0":10000000000000000000}]]` | A list of keyword arguments for the strategies. | 350 | | `USE_SUBGRAPH_FOR_REDEEMING` | `bool` | `true` | Whether to use the subgraph to check if a position is redeemed. | 351 | | `USE_NEVERMINED` | `bool` | `false` | Whether to use Nevermined. | 352 | | `SUBSCRIPTION_PARAMS` | `list` | `[["base_url", "https://marketplace-api.gnosis.nevermined.app/api/v1/metadata/assets/ddo"],["did", "did:nv:01706149da2f9f3f67cf79ec86c37d63cec87fc148f5633b12bf6695653d5b3c"],["escrow_payment_condition_address", "0x31B2D187d674C9ACBD2b25f6EDce3d2Db2B7f446"],["lock_payment_condition_address", "0x2749DDEd394196835199471027713773736bffF2"],["transfer_nft_condition_address", "0x659fCA7436936e9fe8383831b65B8B442eFc8Ea8"],["token_address", "0x1b5DeaD7309b56ca7663b3301A503e077Be18cba"], ["order_address","0x72201948087aE83f8Eac22cf7A9f2139e4cFA829"], ["nft_amount", "100"], ["payment_token","0x0000000000000000000000000000000000000000"], ["order_address", "0x72201948087aE83f8Eac22cf7A9f2139e4cFA829"],["price", "1000000000000000000"]]` | Parameters for the Nevermined subscription. | 353 | 354 | The rest of the common environment variables are present in the [service.yaml](https://github.com/valory-xyz/trader/blob/v0.18.2/packages/valory/services/trader/service.yaml), which are customizable too. 355 | 356 | ##### Checking agents' health 357 | 358 | You may check the health of the agents by querying the `/healthcheck` endpoint. For example: 359 | 360 | ```shell 361 | curl -sL localhost:8716/healthcheck | jq -C 362 | ``` 363 | 364 | This will return a JSON output with the following fields: 365 | 366 | | Field | Type | Criteria | 367 | | --- | --- | --- | 368 | | `seconds_since_last_transition` | float | The number of seconds passed since the last transition in the FSM. | 369 | | `is_tm_healthy` | bool | `false` if more than `BLOCKS_STALL_TOLERANCE` (60) seconds have passed since the last begin block request received from the tendermint node. `true` otherwise. | 370 | | `period` | int | The number of full cycles completed in the FSM. | 371 | | `reset_pause_duration` | int | The number of seconds to wait before starting the next FSM cycle. | 372 | | `rounds` | list | The last rounds (upto 25) in the FSM that happened including the current one. | 373 | | `is_transitioning_fast` | bool | `true` if `is_tm_healthy` is `true` and `seconds_since_last_transition` is less than twice the `reset_pause_duration`. `false` otherwise. | 374 | 375 | So, you can usually use `is_transitioning_fast` as a rule to check if an agent is healthly. To add a more strict check, you can also tune a threshold for the `seconds_since_last_transition` and rate of change of `period`, but that will require some monitoring to fine tune it. 376 | 377 | ## Backup and Recovery 378 | 379 | When executed for the first time, the `run_service.sh` script creates a number of Gnosis chain accounts: 380 | 381 | - one EOA account will be used as the service owner and agent operator, 382 | - one EOA account will be used for the trading agent, and 383 | - one smart contract account corresponds to a [Safe](https://app.safe.global/) wallet with a single owner (the agent account). 384 | 385 | The addresses and private keys of the EOA accounts (plus some additional configuration) are stored within the folder `.trader_runner`. In order to avoid losing your assets, back up this folder in a safe place, and do not publish or share its contents with unauthorized parties. 386 | 387 | You can gain access to the assets of your service as follows: 388 | 389 | 1. Ensure that your service is stopped by running `stop_service.sh`. 390 | 2. Ensure that you have a hot wallet (e.g., [MetaMask](https://metamask.io/)) installed and set up in your browser. 391 | 3. Import the two EOAs accounts using the private keys. In MetaMask, select "Add account or hardware wallet" → "Import account" → "Select Type: Private Key", and enter the private key of the owner/operator EOA account (located in `.trader_runner/operator_pkey.txt`): 392 | MetaMask import private key 393 | 394 | 4. Repeat the same process with the agent EOA account (private key located in `.trader_runner/agent_pkey.json`). 395 | 396 | Now, you have full access through the hot wallet to the EOAs addresses associated to your service and you can transfer their assets to any other address. You can also manage the assets of the service Safe through the DApp https://app.safe.global/, using the address located in the file `.trader_runner/service_safe_address.txt`. 397 | 398 | ## Terminate your on-chain service 399 | 400 | If you wish to terminate your on-chain service (and receive back the staking/bonding funds to your owner/operator address in case your service is staked) execute: 401 | 402 | ```bash 403 | ./stop_service.sh 404 | ./terminate_on_chain_service.sh 405 | ``` 406 | 407 | ## RPC-related Error Messages 408 | 409 | When updating the service, you may need to re-run the script if you obtain any of the following error messages: 410 | 411 | ```Error: Service terminatation failed with following error; ChainTimeoutError(Timed out when waiting for transaction to go through) 412 | 413 | Error: Service unbonding failed with following error; ChainTimeoutError(Timed out when waiting for transaction to go through) 414 | 415 | Error: Component mint failed with following error; ChainTimeoutError(Timed out when waiting for transaction to go through) 416 | 417 | Error: Service activation failed with following error; ChainTimeoutError(Timed out when waiting for transaction to go through) 418 | 419 | Error: Service deployment failed with following error; ChainTimeoutError(Timed out when waiting for transaction to go through) 420 | 421 | Error: Service terminatation failed with following error; ChainInteractionError({'code': -32010, 'message': 'AlreadyKnown'}) 422 | ``` 423 | 424 | ## Build deployments without executing the service 425 | 426 | The script builds both a Docker Compose deployment (on `./trader/trader_service/abci_build_????`) 427 | and a Kubernetes deployment (on `./trader/trader_service/abci_build_k8s`). 428 | Then, by default, the script will launch the local Docker Compose deployment. 429 | If you just want to build the deployment without executing the service 430 | (for example, if you are deploying to a custom Kubernetes cluster), then execute the script as: 431 | 432 | ```bash 433 | ./run_service.sh --build-only 434 | ``` 435 | -------------------------------------------------------------------------------- /analyse_logs.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | import sys 4 | import argparse 5 | 6 | 7 | def _parse_args(): 8 | """Parse the script arguments.""" 9 | parser = argparse.ArgumentParser(description="Analyse agent logs.") 10 | 11 | parser.add_argument( 12 | "--service-dir", 13 | default="trader_service", 14 | help="The service directory containing build directories (default: 'trader_service')." 15 | ) 16 | parser.add_argument( 17 | "--from-dir", 18 | help="Path to the logs directory. If not provided, it is auto-detected." 19 | ) 20 | parser.add_argument( 21 | "--agent", 22 | default="aea_0", 23 | help="The agent name to analyze (default: 'aea_0')." 24 | ) 25 | parser.add_argument( 26 | "--reset-db", 27 | action="store_true", 28 | help="Use this flag to disable resetting the log database." 29 | ) 30 | parser.add_argument( 31 | "--start-time", 32 | help="Start time in `YYYY-MM-DD H:M:S,MS` format." 33 | ) 34 | parser.add_argument( 35 | "--end-time", 36 | help="End time in `YYYY-MM-DD H:M:S,MS` format." 37 | ) 38 | parser.add_argument( 39 | "--log-level", 40 | choices=["INFO", "DEBUG", "WARNING", "ERROR", "CRITICAL"], 41 | help="Logging level." 42 | ) 43 | parser.add_argument( 44 | "--period", 45 | type=int, 46 | help="Period ID." 47 | ) 48 | parser.add_argument( 49 | "--round", 50 | help="Round name." 51 | ) 52 | parser.add_argument( 53 | "--behaviour", 54 | help="Behaviour name filter." 55 | ) 56 | parser.add_argument( 57 | "--fsm", 58 | action="store_true", 59 | help="Print only the FSM execution path." 60 | ) 61 | parser.add_argument( 62 | "--include-regex", 63 | help="Regex pattern to include in the result." 64 | ) 65 | parser.add_argument( 66 | "--exclude-regex", 67 | help="Regex pattern to exclude from the result." 68 | ) 69 | 70 | return parser.parse_args() 71 | 72 | 73 | def find_build_directory(service_dir): 74 | """Find the appropriate build directory within the service directory.""" 75 | try: 76 | # create a list of all build directories 77 | build_dirs = [ 78 | d for d in os.listdir(service_dir) 79 | if d.startswith("abci_build_") and os.path.isdir(os.path.join(service_dir, d)) 80 | ] 81 | # iterate through the build directories to find the one that contains logs 82 | for build_dir in build_dirs: 83 | build_dir = os.path.join(service_dir, build_dir) 84 | logs_dir = os.path.join(build_dir, "persistent_data", "logs") 85 | # Check if the logs folder exists and contains files 86 | if os.path.exists(logs_dir) and os.listdir(logs_dir): 87 | return build_dir 88 | return os.path.join(service_dir, "abci_build") 89 | except FileNotFoundError: 90 | print(f"Service directory '{service_dir}' not found") 91 | sys.exit(1) 92 | 93 | 94 | def run_analysis(logs_dir, **kwargs): 95 | """Run the log analysis command.""" 96 | command = [ 97 | "poetry", "run", "autonomy", "analyse", "logs", 98 | "--from-dir", logs_dir, 99 | ] 100 | if kwargs.get("agent"): 101 | command.extend(["--agent", kwargs.get("agent")]) 102 | if kwargs.get("reset_db"): 103 | command.extend(["--reset-db"]) 104 | if kwargs.get("start_time"): 105 | command.extend(["--start-time", kwargs.get("start_time")]) 106 | if kwargs.get("end_time"): 107 | command.extend(["--end-time", kwargs.get("end_time")]) 108 | if kwargs.get("log_level"): 109 | command.extend(["--log-level", kwargs.get("log_level")]) 110 | if kwargs.get("period"): 111 | command.extend(["--period", kwargs.get("period")]) 112 | if kwargs.get("round"): 113 | command.extend(["--round", kwargs.get("round")]) 114 | if kwargs.get("behaviour"): 115 | command.extend(["--behaviour", kwargs.get("behaviour")]) 116 | if kwargs.get("fsm"): 117 | command.extend(["--fsm"]) 118 | if kwargs.get("include_regex"): 119 | command.extend(["--include-regex", kwargs.get("include_regex")]) 120 | if kwargs.get("exclude_regex"): 121 | command.extend(["--exclude-regex", kwargs.get("exclude_regex")]) 122 | 123 | try: 124 | subprocess.run(command, check=True) 125 | print("Analysis completed successfully.") 126 | except subprocess.CalledProcessError as e: 127 | print(f"Command failed with exit code {e.returncode}") 128 | sys.exit(e.returncode) 129 | except FileNotFoundError: 130 | print("Poetry or autonomy not found. Ensure they are installed and accessible.") 131 | sys.exit(1) 132 | 133 | 134 | if __name__ == "__main__": 135 | # Parse user arguments 136 | args = _parse_args() 137 | 138 | # Determine the logs directory 139 | if args.from_dir: 140 | logs_dir = args.from_dir 141 | if not os.path.exists(logs_dir): 142 | print(f"Specified logs directory '{logs_dir}' not found.") 143 | sys.exit(1) 144 | else: 145 | # Auto-detect the logs directory 146 | build_dir = find_build_directory(args.service_dir) 147 | logs_dir = os.path.join(build_dir, "persistent_data", "logs") 148 | if not os.path.exists(logs_dir): 149 | print(f"Logs directory '{logs_dir}' not found.") 150 | sys.exit(1) 151 | 152 | # Run the analysis 153 | run_analysis(logs_dir, **vars(args)) 154 | -------------------------------------------------------------------------------- /claim_staking_rewards.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # ------------------------------------------------------------------------------ 4 | # 5 | # Copyright 2023-2024 Valory AG 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | # ------------------------------------------------------------------------------ 20 | 21 | cd trader; poetry run python "../scripts/claim_staking_rewards.py"; cd .. 22 | -------------------------------------------------------------------------------- /images/docker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valory-xyz/trader-quickstart/1274e7992b107ffbde07d8e7327bcf3e1393b474/images/docker.png -------------------------------------------------------------------------------- /images/metamask_import_private_key.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valory-xyz/trader-quickstart/1274e7992b107ffbde07d8e7327bcf3e1393b474/images/metamask_import_private_key.png -------------------------------------------------------------------------------- /images/staking_fsm.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 |
Unstaked
Unstaked
Staked
Staked
Evicted
Evicted
stake()
stake()
unstake()
unstake...
unstake()
unstake...
checkpoint()
checkpo...
checkpoint()
checkpo...
Inactive
Inactive
Text is not SVG - cannot display
-------------------------------------------------------------------------------- /images/trader_fsm_transitions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valory-xyz/trader-quickstart/1274e7992b107ffbde07d8e7327bcf3e1393b474/images/trader_fsm_transitions.png -------------------------------------------------------------------------------- /rank_traders.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # ------------------------------------------------------------------------------ 4 | # 5 | # Copyright 2022-2023 Valory AG 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | # ------------------------------------------------------------------------------ 20 | 21 | """This script queries the OMEN subgraph to obtain the trades of a given address.""" 22 | 23 | import datetime 24 | import os 25 | import sys 26 | from argparse import ArgumentParser 27 | from collections import defaultdict 28 | from dotenv import load_dotenv 29 | from pathlib import Path 30 | from string import Template 31 | from typing import Any 32 | 33 | import requests 34 | import trades 35 | from trades import MarketAttribute, MarketState, wei_to_xdai 36 | 37 | 38 | QUERY_BATCH_SIZE = 1000 39 | DUST_THRESHOLD = 10000000000000 40 | INVALID_ANSWER = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF 41 | FPMM_CREATOR = "0x89c5cc945dd550bcffb72fe42bff002429f46fec" 42 | DEFAULT_FROM_DATE = "1970-01-01T00:00:00" 43 | DEFAULT_TO_DATE = "2038-01-19T03:14:07" 44 | SCRIPT_PATH = Path(__file__).resolve().parent 45 | STORE_PATH = Path(SCRIPT_PATH, ".trader_runner") 46 | ENV_FILE = Path(STORE_PATH, ".env") 47 | 48 | load_dotenv(ENV_FILE) 49 | 50 | 51 | headers = { 52 | "Accept": "application/json, multipart/mixed", 53 | "Content-Type": "application/json", 54 | } 55 | 56 | 57 | omen_xdai_trades_query = Template( 58 | """ 59 | { 60 | fpmmTrades( 61 | where: { 62 | type: Buy, 63 | fpmm_: { 64 | creator: "${fpmm_creator}" 65 | creationTimestamp_gte: "${fpmm_creationTimestamp_gte}", 66 | creationTimestamp_lt: "${fpmm_creationTimestamp_lte}" 67 | }, 68 | creationTimestamp_gte: "${creationTimestamp_gte}", 69 | creationTimestamp_lte: "${creationTimestamp_lte}" 70 | id_gt: "${id_gt}" 71 | } 72 | first: ${first} 73 | orderBy: id 74 | orderDirection: asc 75 | ) { 76 | id 77 | title 78 | collateralToken 79 | outcomeTokenMarginalPrice 80 | oldOutcomeTokenMarginalPrice 81 | type 82 | creator { 83 | id 84 | } 85 | creationTimestamp 86 | collateralAmount 87 | collateralAmountUSD 88 | feeAmount 89 | outcomeIndex 90 | outcomeTokensTraded 91 | transactionHash 92 | fpmm { 93 | id 94 | outcomes 95 | title 96 | answerFinalizedTimestamp 97 | currentAnswer 98 | isPendingArbitration 99 | arbitrationOccurred 100 | openingTimestamp 101 | condition { 102 | id 103 | } 104 | } 105 | } 106 | } 107 | """ 108 | ) 109 | 110 | ATTRIBUTE_CHOICES = {i.name: i for i in MarketAttribute} 111 | 112 | 113 | def _parse_args() -> Any: 114 | """Parse the creator positional argument.""" 115 | parser = ArgumentParser(description="Get trades on Omen for a Safe address.") 116 | parser.add_argument( 117 | "--from-date", 118 | type=datetime.datetime.fromisoformat, 119 | default=DEFAULT_FROM_DATE, 120 | help="Start date for trades (UTC) in YYYY-MM-DD:HH:mm:ss format", 121 | ) 122 | parser.add_argument( 123 | "--to-date", 124 | type=datetime.datetime.fromisoformat, 125 | default=DEFAULT_TO_DATE, 126 | help="End date for trades (UTC) in YYYY-MM-DD:HH:mm:ss format", 127 | ) 128 | parser.add_argument( 129 | "--fpmm-created-from-date", 130 | type=datetime.datetime.fromisoformat, 131 | default=DEFAULT_FROM_DATE, 132 | help="Start date for market open date (UTC) in YYYY-MM-DD:HH:mm:ss format", 133 | ) 134 | parser.add_argument( 135 | "--fpmm-created-to-date", 136 | type=datetime.datetime.fromisoformat, 137 | default=DEFAULT_TO_DATE, 138 | help="End date for market open date (UTC) in YYYY-MM-DD:HH:mm:ss format", 139 | ) 140 | parser.add_argument( 141 | "--sort-by", 142 | choices=list(MarketAttribute), 143 | default=MarketAttribute.ROI, 144 | type=MarketAttribute.argparse, 145 | help="Specify the market attribute for sorting.", 146 | ) 147 | args = parser.parse_args() 148 | 149 | args.from_date = args.from_date.replace(tzinfo=datetime.timezone.utc) 150 | args.to_date = args.to_date.replace(tzinfo=datetime.timezone.utc) 151 | args.fpmm_created_from_date = args.fpmm_created_from_date.replace( 152 | tzinfo=datetime.timezone.utc 153 | ) 154 | args.fpmm_created_to_date = args.fpmm_created_to_date.replace( 155 | tzinfo=datetime.timezone.utc 156 | ) 157 | 158 | return args 159 | 160 | 161 | def _to_content(q: str) -> dict[str, Any]: 162 | """Convert the given query string to payload content, i.e., add it under a `queries` key and convert it to bytes.""" 163 | finalized_query = { 164 | "query": q, 165 | "variables": None, 166 | "extensions": {"headers": None}, 167 | } 168 | return finalized_query 169 | 170 | 171 | def _query_omen_xdai_subgraph( 172 | from_timestamp: float, 173 | to_timestamp: float, 174 | fpmm_from_timestamp: float, 175 | fpmm_to_timestamp: float, 176 | ) -> dict[str, Any]: 177 | """Query the subgraph.""" 178 | subgraph_api_key = os.getenv('SUBGRAPH_API_KEY') 179 | url = f"https://gateway-arbitrum.network.thegraph.com/api/{subgraph_api_key}/subgraphs/id/9fUVQpFwzpdWS9bq5WkAnmKbNNcoBwatMR4yZq81pbbz" 180 | 181 | grouped_results = defaultdict(list) 182 | id_gt = "" 183 | 184 | while True: 185 | query = omen_xdai_trades_query.substitute( 186 | fpmm_creator=FPMM_CREATOR.lower(), 187 | creationTimestamp_gte=int(from_timestamp), 188 | creationTimestamp_lte=int(to_timestamp), 189 | fpmm_creationTimestamp_gte=int(fpmm_from_timestamp), 190 | fpmm_creationTimestamp_lte=int(fpmm_to_timestamp), 191 | first=QUERY_BATCH_SIZE, 192 | id_gt=id_gt, 193 | ) 194 | content_json = _to_content(query) 195 | res = requests.post(url, headers=headers, json=content_json) 196 | result_json = res.json() 197 | user_trades = result_json.get("data", {}).get("fpmmTrades", []) 198 | 199 | if not user_trades: 200 | break 201 | 202 | for trade in user_trades: 203 | fpmm_id = trade.get("fpmm", {}).get("id") 204 | grouped_results[fpmm_id].append(trade) 205 | 206 | id_gt = user_trades[len(user_trades) - 1]["id"] 207 | 208 | all_results = { 209 | "data": { 210 | "fpmmTrades": [ 211 | trade 212 | for trades_list in grouped_results.values() 213 | for trade in trades_list 214 | ] 215 | } 216 | } 217 | 218 | return all_results 219 | 220 | 221 | def _group_trades_by_creator(trades_json: dict[str, Any]) -> dict[str, Any]: 222 | """Group trades by creator ID from the given JSON data.""" 223 | 224 | fpmm_trades = trades_json["data"]["fpmmTrades"] 225 | trades_by_creator = defaultdict(list) 226 | 227 | for trade in fpmm_trades: 228 | _creator_id = trade["creator"]["id"] 229 | trades_by_creator[_creator_id].append(trade) 230 | 231 | _creator_to_trades = { 232 | creator_id: {"data": {"fpmmTrades": trades}} 233 | for creator_id, trades in trades_by_creator.items() 234 | } 235 | return _creator_to_trades 236 | 237 | 238 | def _print_user_summary( 239 | creator_to_statistics: dict[str, Any], 240 | sort_by_attribute: MarketAttribute = MarketAttribute.ROI, 241 | state: MarketState = MarketState.CLOSED, 242 | ) -> None: 243 | """Prints user ranking.""" 244 | 245 | sorted_users = sorted( 246 | creator_to_statistics.items(), 247 | key=lambda item: item[1][sort_by_attribute][state], 248 | reverse=True, 249 | ) 250 | 251 | print("") 252 | title = f"User summary for {state} markets sorted by {sort_by_attribute}:" 253 | print() 254 | print("-" * len(title)) 255 | print(title) 256 | print("-" * len(title)) 257 | print("") 258 | 259 | titles = [ 260 | "User ID".ljust(42), 261 | "Ntrades".rjust(8), 262 | "Nwins".rjust(8), 263 | "Nredem".rjust(8), 264 | "Investment".rjust(13), 265 | "Fees".rjust(13), 266 | "Earnings".rjust(13), 267 | "Net Earn.".rjust(13), 268 | "Redemptions".rjust(13), 269 | "ROI".rjust(9), 270 | "\n", 271 | ] 272 | 273 | output = "".join(titles) 274 | for user_id, statistics_table in sorted_users: 275 | values = [ 276 | user_id, 277 | str(statistics_table[MarketAttribute.NUM_TRADES][state]).rjust(8), 278 | str(statistics_table[MarketAttribute.WINNER_TRADES][state]).rjust(8), 279 | str(statistics_table[MarketAttribute.NUM_REDEEMED][state]).rjust(8), 280 | wei_to_xdai(statistics_table[MarketAttribute.INVESTMENT][state]).rjust(13), 281 | wei_to_xdai(statistics_table[MarketAttribute.FEES][state]).rjust(13), 282 | wei_to_xdai(statistics_table[MarketAttribute.EARNINGS][state]).rjust(13), 283 | wei_to_xdai(statistics_table[MarketAttribute.NET_EARNINGS][state]).rjust(13), 284 | wei_to_xdai(statistics_table[MarketAttribute.REDEMPTIONS][state]).rjust(13), 285 | f"{statistics_table[MarketAttribute.ROI][state] * 100.0:7.2f}%".rjust(9), 286 | "\n", 287 | ] 288 | output += "".join(values) 289 | 290 | print(output) 291 | 292 | 293 | def _print_progress_bar( # pylint: disable=too-many-arguments 294 | iteration: int, 295 | total: int, 296 | prefix: str = "Computing statistics:", 297 | suffix: str = "Complete", 298 | length: int = 50, 299 | fill: str = "#", 300 | ) -> None: 301 | """Prints the progress bar""" 302 | if len(fill) != 1: 303 | raise ValueError("Fill character must be a single character.") 304 | 305 | percent = ("{0:.1f}").format(100 * (iteration / float(total))) 306 | filled_length = int(length * iteration // total) 307 | bar = fill * filled_length + "-" * (length - filled_length) 308 | progress_string = f"({iteration} of {total}) - {percent}%" 309 | sys.stdout.write("\r%s |%s| %s %s" % (prefix, bar, progress_string, suffix)) 310 | sys.stdout.flush() 311 | 312 | 313 | if __name__ == "__main__": 314 | print("Starting script") 315 | user_args = _parse_args() 316 | 317 | with open(trades.RPC_PATH, "r", encoding="utf-8") as rpc_file: 318 | rpc = rpc_file.read() 319 | 320 | print("Querying Thegraph...") 321 | all_trades_json = _query_omen_xdai_subgraph( 322 | user_args.from_date.timestamp(), 323 | user_args.to_date.timestamp(), 324 | user_args.fpmm_created_from_date.timestamp(), 325 | user_args.fpmm_created_to_date.timestamp(), 326 | ) 327 | print(f'Total trading transactions: {len(all_trades_json["data"]["fpmmTrades"])}') 328 | 329 | creator_to_trades = _group_trades_by_creator(all_trades_json) 330 | total_traders = len(creator_to_trades.items()) 331 | print(f"Total traders: {total_traders}") 332 | 333 | creator_to_statistics = {} 334 | _print_progress_bar(0, total_traders) 335 | for i, (creator_id, trades_json_id) in enumerate( 336 | creator_to_trades.items(), start=1 337 | ): 338 | _, statistics_table_id = trades.parse_user(rpc, creator_id, trades_json_id, {}) 339 | creator_to_statistics[creator_id] = statistics_table_id 340 | _print_progress_bar(i, total_traders) 341 | 342 | _print_user_summary(creator_to_statistics, user_args.sort_by) 343 | -------------------------------------------------------------------------------- /report.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # ------------------------------------------------------------------------------ 4 | # 5 | # Copyright 2022-2023 Valory AG 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | # ------------------------------------------------------------------------------ 20 | 21 | """Obtains a report of the current service.""" 22 | 23 | import json 24 | import math 25 | import time 26 | import traceback 27 | from argparse import ArgumentParser 28 | from collections import Counter 29 | 30 | from dotenv import dotenv_values 31 | from enum import Enum 32 | from pathlib import Path 33 | from typing import Any 34 | 35 | import docker 36 | import trades 37 | from trades import ( 38 | MarketAttribute, 39 | MarketState, 40 | get_balance, 41 | get_token_balance, 42 | wei_to_olas, 43 | wei_to_unit, 44 | wei_to_wxdai, 45 | wei_to_xdai, 46 | ) 47 | from web3 import HTTPProvider, Web3 48 | 49 | 50 | SCRIPT_PATH = Path(__file__).resolve().parent 51 | STORE_PATH = Path(SCRIPT_PATH, ".trader_runner") 52 | DOTENV_PATH = Path(STORE_PATH, ".env") 53 | RPC_PATH = Path(STORE_PATH, "rpc.txt") 54 | AGENT_KEYS_JSON_PATH = Path(STORE_PATH, "keys.json") 55 | OPERATOR_KEYS_JSON_PATH = Path(STORE_PATH, "operator_keys.json") 56 | SAFE_ADDRESS_PATH = Path(STORE_PATH, "service_safe_address.txt") 57 | SERVICE_ID_PATH = Path(STORE_PATH, "service_id.txt") 58 | STAKING_TOKEN_JSON_PATH = Path( 59 | SCRIPT_PATH, 60 | "trader", 61 | "packages", 62 | "valory", 63 | "contracts", 64 | "staking_token", 65 | "build", 66 | "StakingToken.json", 67 | ) 68 | ACTIVITY_CHECKER_JSON_PATH = Path( 69 | SCRIPT_PATH, 70 | "trader", 71 | "packages", 72 | "valory", 73 | "contracts", 74 | "mech_activity", 75 | "build", 76 | "MechActivity.json", 77 | ) 78 | SERVICE_REGISTRY_L2_JSON_PATH = Path( 79 | SCRIPT_PATH, 80 | "trader", 81 | "packages", 82 | "valory", 83 | "contracts", 84 | "service_registry", 85 | "build", 86 | "ServiceRegistryL2.json", 87 | ) 88 | SERVICE_REGISTRY_TOKEN_UTILITY_JSON_PATH = Path( 89 | SCRIPT_PATH, 90 | "contracts", 91 | "ServiceRegistryTokenUtility.json", 92 | ) 93 | MECH_CONTRACT_ADDRESS = "0x77af31De935740567Cf4fF1986D04B2c964A786a" 94 | MECH_CONTRACT_JSON_PATH = Path( 95 | SCRIPT_PATH, 96 | "trader", 97 | "packages", 98 | "valory", 99 | "contracts", 100 | "mech", 101 | "build", 102 | "mech.json", 103 | ) 104 | 105 | SAFE_BALANCE_THRESHOLD = 500000000000000000 106 | AGENT_XDAI_BALANCE_THRESHOLD = 50000000000000000 107 | OPERATOR_XDAI_BALANCE_THRESHOLD = 50000000000000000 108 | MECH_REQUESTS_PER_EPOCH_THRESHOLD = 10 109 | TRADES_LOOKBACK_DAYS = 3 110 | MULTI_TRADE_LOOKBACK_DAYS = TRADES_LOOKBACK_DAYS 111 | SECONDS_PER_DAY = 60 * 60 * 24 112 | 113 | OUTPUT_WIDTH = 80 114 | TRADER_CONTAINER_PREFIX = "trader" 115 | AGENT_CONTAINER_IDENTIFIER = "abci" 116 | NODE_CONTAINER_IDENTIFIER = "tm" 117 | 118 | 119 | class ColorCode: 120 | """Terminal color codes""" 121 | 122 | GREEN = "\033[92m" 123 | RED = "\033[91m" 124 | YELLOW = "\033[93m" 125 | RESET = "\033[0m" 126 | 127 | 128 | class StakingState(Enum): 129 | """Staking state enumeration for the staking.""" 130 | 131 | UNSTAKED = 0 132 | STAKED = 1 133 | EVICTED = 2 134 | 135 | 136 | def _color_string(text: str, color_code: str) -> str: 137 | return f"{color_code}{text}{ColorCode.RESET}" 138 | 139 | 140 | def _color_bool( 141 | is_true: bool, true_string: str = "True", false_string: str = "False" 142 | ) -> str: 143 | if is_true: 144 | return _color_string(true_string, ColorCode.GREEN) 145 | return _color_string(false_string, ColorCode.RED) 146 | 147 | 148 | def _color_percent(p: float, multiplier: float = 100, symbol: str = "%") -> str: 149 | if p >= 0: 150 | return f"{p*multiplier:.2f} {symbol}" 151 | return _color_string(f"{p*multiplier:.2f} {symbol}", ColorCode.RED) 152 | 153 | 154 | def _trades_since_message(trades_json: dict[str, Any], utc_ts: float = 0) -> str: 155 | filtered_trades = [ 156 | trade 157 | for trade in trades_json.get("data", {}).get("fpmmTrades", []) 158 | if float(trade["creationTimestamp"]) >= utc_ts 159 | ] 160 | unique_markets = set(trade["fpmm"]["id"] for trade in filtered_trades) 161 | trades_count = len(filtered_trades) 162 | markets_count = len(unique_markets) 163 | return f"{trades_count} trades on {markets_count} markets" 164 | 165 | 166 | def _calculate_retrades_since(trades_json: dict[str, Any], utc_ts: float = 0) -> tuple[Counter[Any], int, int, int]: 167 | filtered_trades = Counter(( 168 | trade.get("fpmm", {}).get("id", None) 169 | for trade in trades_json.get("data", {}).get("fpmmTrades", []) 170 | if float(trade.get("creationTimestamp", 0)) >= utc_ts 171 | )) 172 | 173 | if None in filtered_trades: 174 | raise ValueError( 175 | f"Unexpected format in trades_json: {filtered_trades[None]} trades have no associated market ID.") 176 | 177 | unique_markets = set(filtered_trades) 178 | n_unique_markets = len(unique_markets) 179 | n_trades = sum(filtered_trades.values()) 180 | n_retrades = sum(n_bets - 1 for n_bets in filtered_trades.values() if n_bets > 1) 181 | 182 | return filtered_trades, n_unique_markets, n_trades, n_retrades 183 | 184 | def _retrades_since_message(n_unique_markets: int, n_trades: int, n_retrades: int) -> str: 185 | return f"{n_retrades} re-trades on total {n_trades} trades in {n_unique_markets} markets" 186 | 187 | def _average_trades_since_message(n_trades: int, n_markets: int) -> str: 188 | if not n_markets: 189 | average_trades = 0 190 | else: 191 | average_trades = round(n_trades / n_markets, 2) 192 | 193 | return f"{average_trades} trades per market" 194 | 195 | def _max_trades_per_market_since_message(filtered_trades: Counter[Any]) -> str: 196 | if not filtered_trades: 197 | max_trades = 0 198 | else: 199 | max_trades = max(filtered_trades.values()) 200 | 201 | return f"{max_trades} trades per market" 202 | 203 | 204 | def _get_mech_requests_count( 205 | mech_requests: dict[str, Any], timestamp: float = 0 206 | ) -> int: 207 | return sum( 208 | 1 209 | for mech_request in mech_requests.values() 210 | if mech_request.get("block_timestamp", 0) > timestamp 211 | ) 212 | 213 | 214 | def _print_section_header(header: str) -> None: 215 | print("\n\n" + header) 216 | print("=" * OUTPUT_WIDTH) 217 | 218 | 219 | def _print_subsection_header(header: str) -> None: 220 | print("\n" + header) 221 | print("-" * OUTPUT_WIDTH) 222 | 223 | 224 | def _print_status(key: str, value: str, message: str = "") -> None: 225 | print(f"{key:<30}{value:<10} {message or ''}") 226 | 227 | 228 | def _warning_message(current_value: int, threshold: int = 0, message: str = "") -> str: 229 | default_message = _color_string( 230 | f"- Balance too low. Threshold is {wei_to_unit(threshold):.2f}.", 231 | ColorCode.YELLOW, 232 | ) 233 | if current_value < threshold: 234 | return ( 235 | _color_string(f"{message}", ColorCode.YELLOW) 236 | if message 237 | else default_message 238 | ) 239 | return "" 240 | 241 | 242 | def _get_agent_status() -> str: 243 | client = docker.from_env() 244 | agent_running = node_running = service_running = False 245 | for container in client.containers.list(): 246 | container_name = container.name 247 | if TRADER_CONTAINER_PREFIX in container_name: 248 | if AGENT_CONTAINER_IDENTIFIER in container_name: 249 | agent_running = True 250 | if NODE_CONTAINER_IDENTIFIER in container_name: 251 | node_running = True 252 | if agent_running and node_running: 253 | service_running = True 254 | break 255 | 256 | return _color_bool(service_running, "Running", "Stopped") 257 | 258 | 259 | def _parse_args() -> Any: 260 | """Parse the script arguments.""" 261 | parser = ArgumentParser(description="Get a report for a trader service.") 262 | args = parser.parse_args() 263 | return args 264 | 265 | 266 | if __name__ == "__main__": 267 | user_args = _parse_args() 268 | 269 | with open(AGENT_KEYS_JSON_PATH, "r", encoding="utf-8") as file: 270 | agent_keys_data = json.load(file) 271 | agent_address = agent_keys_data[0]["address"] 272 | 273 | with open(OPERATOR_KEYS_JSON_PATH, "r", encoding="utf-8") as file: 274 | operator_keys_data = json.load(file) 275 | operator_address = operator_keys_data[0]["address"] 276 | 277 | with open(SAFE_ADDRESS_PATH, "r", encoding="utf-8") as file: 278 | safe_address = file.read().strip() 279 | 280 | with open(SERVICE_ID_PATH, "r", encoding="utf-8") as file: 281 | service_id = int(file.read().strip()) 282 | 283 | with open(RPC_PATH, "r", encoding="utf-8") as file: 284 | rpc = file.read().strip() 285 | 286 | env_file_vars = dotenv_values(DOTENV_PATH) 287 | 288 | # Prediction market trading 289 | mech_requests = trades.get_mech_requests(safe_address) 290 | mech_statistics = trades.get_mech_statistics(mech_requests) 291 | trades_json = trades._query_omen_xdai_subgraph(safe_address) 292 | _, statistics_table = trades.parse_user( 293 | rpc, safe_address, trades_json, mech_statistics 294 | ) 295 | 296 | print("") 297 | print("==============") 298 | print("Service report") 299 | print("==============") 300 | 301 | # Performance 302 | _print_section_header("Performance") 303 | _print_subsection_header("Staking") 304 | 305 | try: 306 | w3 = Web3(HTTPProvider(rpc)) 307 | 308 | staking_token_address = env_file_vars.get("CUSTOM_STAKING_ADDRESS") 309 | with open(STAKING_TOKEN_JSON_PATH, "r", encoding="utf-8") as file: 310 | staking_token_data = json.load(file) 311 | 312 | staking_token_abi = staking_token_data.get("abi", []) 313 | staking_token_contract = w3.eth.contract( 314 | address=staking_token_address, abi=staking_token_abi # type: ignore 315 | ) 316 | 317 | staking_state = StakingState( 318 | staking_token_contract.functions.getStakingState( 319 | service_id 320 | ).call() 321 | ) 322 | 323 | is_staked = ( 324 | staking_state == StakingState.STAKED 325 | or staking_state == StakingState.EVICTED 326 | ) 327 | _print_status("Is service staked?", _color_bool(is_staked, "Yes", "No")) 328 | if is_staked: 329 | _print_status("Staking program", env_file_vars.get("STAKING_PROGRAM")) # type: ignore 330 | if staking_state == StakingState.STAKED: 331 | _print_status("Staking state", staking_state.name) 332 | elif staking_state == StakingState.EVICTED: 333 | _print_status("Staking state", _color_string(staking_state.name, ColorCode.RED)) 334 | 335 | if is_staked: 336 | 337 | activity_checker_address = staking_token_contract.functions.activityChecker().call() 338 | with open(ACTIVITY_CHECKER_JSON_PATH, "r", encoding="utf-8") as file: 339 | activity_checker_data = json.load(file) 340 | 341 | activity_checker_abi = activity_checker_data.get("abi", []) 342 | activity_checker_contract = w3.eth.contract( 343 | address=activity_checker_address, abi=activity_checker_abi # type: ignore 344 | ) 345 | 346 | with open( 347 | SERVICE_REGISTRY_TOKEN_UTILITY_JSON_PATH, "r", encoding="utf-8" 348 | ) as file: 349 | service_registry_token_utility_data = json.load(file) 350 | 351 | service_registry_token_utility_contract_address = ( 352 | staking_token_contract.functions.serviceRegistryTokenUtility().call() 353 | ) 354 | service_registry_token_utility_abi = ( 355 | service_registry_token_utility_data.get("abi", []) 356 | ) 357 | service_registry_token_utility_contract = w3.eth.contract( 358 | address=service_registry_token_utility_contract_address, 359 | abi=service_registry_token_utility_abi, 360 | ) 361 | 362 | mech_contract_address = env_file_vars.get("MECH_CONTRACT_ADDRESS") 363 | with open(MECH_CONTRACT_JSON_PATH, "r", encoding="utf-8") as file: 364 | mech_contract_data = json.load(file) 365 | 366 | mech_contract_abi = mech_contract_data.get("abi", []) 367 | 368 | mech_contract = w3.eth.contract( 369 | address=mech_contract_address, abi=mech_contract_abi # type: ignore 370 | ) 371 | 372 | security_deposit = ( 373 | service_registry_token_utility_contract.functions.getOperatorBalance( 374 | operator_address, service_id 375 | ).call() 376 | ) 377 | agent_id = int(env_file_vars.get("AGENT_ID").strip()) 378 | agent_bond = service_registry_token_utility_contract.functions.getAgentBond( 379 | service_id, agent_id 380 | ).call() 381 | min_staking_deposit = ( 382 | staking_token_contract.functions.minStakingDeposit().call() 383 | ) 384 | 385 | # In the setting 1 agent instance as of now: minOwnerBond = minStakingDeposit 386 | min_security_deposit = min_staking_deposit 387 | _print_status( 388 | "Staked (security deposit)", 389 | f"{wei_to_olas(security_deposit)} {_warning_message(security_deposit, min_security_deposit)}", 390 | ) 391 | _print_status( 392 | "Staked (agent bond)", 393 | f"{wei_to_olas(agent_bond)} {_warning_message(agent_bond, min_staking_deposit)}", 394 | ) 395 | 396 | service_info = staking_token_contract.functions.mapServiceInfo( 397 | service_id 398 | ).call() 399 | rewards = service_info[3] 400 | _print_status("Accrued rewards", f"{wei_to_olas(rewards)}") 401 | 402 | liveness_ratio = ( 403 | activity_checker_contract.functions.livenessRatio().call() 404 | ) 405 | mech_requests_24h_threshold = math.ceil( 406 | (liveness_ratio * 60 * 60 * 24) / 10**18 407 | ) 408 | 409 | next_checkpoint_ts = ( 410 | staking_token_contract.functions.getNextRewardCheckpointTimestamp().call() 411 | ) 412 | liveness_period = ( 413 | staking_token_contract.functions.livenessPeriod().call() 414 | ) 415 | last_checkpoint_ts = next_checkpoint_ts - liveness_period 416 | 417 | mech_request_count = mech_contract.functions.getRequestsCount(safe_address).call() 418 | mech_request_count_on_last_checkpoint = ( 419 | staking_token_contract.functions.getServiceInfo(service_id).call() 420 | )[2][1] 421 | mech_requests_since_last_cp = mech_request_count - mech_request_count_on_last_checkpoint 422 | # mech_requests_current_epoch = _get_mech_requests_count( 423 | # mech_requests, last_checkpoint_ts 424 | # ) 425 | mech_requests_current_epoch = mech_requests_since_last_cp 426 | _print_status( 427 | "Num. Mech txs current epoch", 428 | f"{mech_requests_current_epoch} {_warning_message(mech_requests_current_epoch, mech_requests_24h_threshold, f'- Too low. Threshold is {mech_requests_24h_threshold}.')}", 429 | ) 430 | 431 | except Exception: # pylint: disable=broad-except 432 | traceback.print_exc() 433 | print("An error occurred while interacting with the staking contract.") 434 | 435 | _print_subsection_header("Prediction market trading") 436 | _print_status( 437 | "ROI on closed markets", 438 | _color_percent(statistics_table[MarketAttribute.ROI][MarketState.CLOSED]), 439 | ) 440 | 441 | since_ts = time.time() - SECONDS_PER_DAY * TRADES_LOOKBACK_DAYS 442 | _print_status( 443 | f"Trades on last {TRADES_LOOKBACK_DAYS} days", 444 | _trades_since_message(trades_json, since_ts), 445 | ) 446 | 447 | #Multi trade strategy 448 | retrades_since_ts = time.time() - SECONDS_PER_DAY * MULTI_TRADE_LOOKBACK_DAYS 449 | filtered_trades, n_unique_markets, n_trades, n_retrades = _calculate_retrades_since(trades_json, retrades_since_ts) 450 | _print_subsection_header(f"Multi-trade markets in previous {MULTI_TRADE_LOOKBACK_DAYS} days") 451 | _print_status(f"Multi-trade markets", _retrades_since_message(n_unique_markets, n_trades, n_retrades)) 452 | _print_status(f"Average trades per market", _average_trades_since_message(n_trades, n_unique_markets)) 453 | _print_status(f"Max trades per market", _max_trades_per_market_since_message(filtered_trades)) 454 | 455 | # Service 456 | _print_section_header("Service") 457 | _print_status("ID", str(service_id)) 458 | 459 | # Agent 460 | agent_status = _get_agent_status() 461 | agent_xdai = get_balance(agent_address, rpc) 462 | _print_subsection_header("Agent") 463 | _print_status("Status (on this machine)", agent_status) 464 | _print_status("Address", agent_address) 465 | _print_status( 466 | "xDAI Balance", 467 | f"{wei_to_xdai(agent_xdai)} {_warning_message(agent_xdai, AGENT_XDAI_BALANCE_THRESHOLD)}", 468 | ) 469 | 470 | # Safe 471 | safe_xdai = get_balance(safe_address, rpc) 472 | safe_wxdai = get_token_balance(safe_address, trades.WXDAI_CONTRACT_ADDRESS, rpc) 473 | _print_subsection_header( 474 | f"Safe {_warning_message(safe_xdai + safe_wxdai, SAFE_BALANCE_THRESHOLD)}" 475 | ) 476 | _print_status("Address", safe_address) 477 | _print_status("xDAI Balance", wei_to_xdai(safe_xdai)) 478 | _print_status("WxDAI Balance", wei_to_wxdai(safe_wxdai)) 479 | 480 | # Owner/Operator 481 | operator_xdai = get_balance(operator_address, rpc) 482 | _print_subsection_header("Owner/Operator") 483 | _print_status("Address", operator_address) 484 | _print_status( 485 | "xDAI Balance", 486 | f"{wei_to_xdai(operator_xdai)} {_warning_message(operator_xdai, OPERATOR_XDAI_BALANCE_THRESHOLD)}", 487 | ) 488 | print("") 489 | -------------------------------------------------------------------------------- /reset_staking.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # force utf mode for python, cause sometimes there are issues with local codepages 4 | export PYTHONUTF8=1 5 | 6 | 7 | # Check if --attended flag is passed 8 | export ATTENDED=true 9 | for arg in "$@"; do 10 | if [ "$arg" = "--attended=false" ]; then 11 | export ATTENDED=false 12 | fi 13 | done 14 | 15 | cd trader; poetry run python ../scripts/choose_staking.py --reset; cd .. 16 | -------------------------------------------------------------------------------- /scripts/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # ------------------------------------------------------------------------------ 3 | # 4 | # Copyright 2021-2023 Valory AG 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # ------------------------------------------------------------------------------ 19 | 20 | """This directory contains scripts for the trader-quickstart repository.""" 21 | -------------------------------------------------------------------------------- /scripts/change_keys_json_password.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # ------------------------------------------------------------------------------ 4 | # 5 | # Copyright 2022-2023 Valory AG 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | # ------------------------------------------------------------------------------ 20 | 21 | """Changes the key files password of the trader store.""" 22 | 23 | import argparse 24 | import json 25 | import tempfile 26 | from pathlib import Path 27 | 28 | from aea.crypto.helpers import DecryptError, KeyIsIncorrect 29 | from aea_ledger_ethereum.ethereum import EthereumCrypto 30 | 31 | 32 | def _change_keys_json_password( 33 | keys_json_path: Path, pkey_txt_path: Path, current_password: str, new_password: str 34 | ) -> None: # pylint: disable=too-many-arguments 35 | keys_json_reencrypeted = [] 36 | keys = json.load(keys_json_path.open("r")) 37 | 38 | with tempfile.TemporaryDirectory() as temp_dir: 39 | for idx, key in enumerate(keys): 40 | temp_file = Path(temp_dir, str(idx)) 41 | temp_file.open("w+", encoding="utf-8").write(str(key["private_key"])) 42 | try: 43 | crypto = EthereumCrypto.load_private_key_from_path( 44 | str(temp_file), password=current_password 45 | ) 46 | 47 | if new_password: 48 | new_private_key_value = ( 49 | f"{json.dumps(crypto.encrypt(new_password))}" 50 | ) 51 | else: 52 | print( 53 | "WARNING: No new password provided. Files will be not encrypted." 54 | ) 55 | new_private_key_value = crypto.key.hex() 56 | 57 | keys_json_reencrypeted.append( 58 | { 59 | "address": crypto.address, 60 | "private_key": new_private_key_value, 61 | } 62 | ) 63 | json.dump(keys_json_reencrypeted, keys_json_path.open("w+"), indent=2) 64 | print(f"Changed password {keys_json_path}") 65 | 66 | with open(pkey_txt_path, "w", encoding="utf-8") as file: 67 | if new_private_key_value.startswith("0x"): 68 | file.write(new_private_key_value[2:]) 69 | else: 70 | file.write(new_private_key_value) 71 | print(f"Ovewritten {pkey_txt_path}") 72 | except (DecryptError, KeyIsIncorrect): 73 | print("Bad password provided.") 74 | except json.decoder.JSONDecodeError: 75 | print( 76 | "Wrong key file format. If key file is not encrypted, do not provide '--current_password' parameter" 77 | ) 78 | 79 | 80 | if __name__ == "__main__": 81 | parser = argparse.ArgumentParser(description="Change key files password.") 82 | parser.add_argument( 83 | "store_path", type=str, help="Path to the trader store directory." 84 | ) 85 | parser.add_argument( 86 | "--current_password", 87 | type=str, 88 | help="Current password. If not provided, it is assumed files are not encrypted.", 89 | ) 90 | parser.add_argument( 91 | "--new_password", 92 | type=str, 93 | help="New password. If not provided, it will decrypt key files.", 94 | ) 95 | args = parser.parse_args() 96 | 97 | for json_file, pkey_file in ( 98 | ("keys", "agent_pkey"), 99 | ("operator_keys", "operator_pkey"), 100 | ): 101 | _change_keys_json_password( 102 | Path(args.store_path, f"{json_file}.json"), 103 | Path(args.store_path, f"{pkey_file}.txt"), 104 | args.current_password, 105 | args.new_password, 106 | ) 107 | print("") 108 | -------------------------------------------------------------------------------- /scripts/check_python.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | if ( 4 | sys.version_info.major != 3 5 | or sys.version_info.minor < 8 6 | or sys.version_info.minor > 11 7 | ): 8 | print( 9 | "Python version >=3.8.0, <3.12.0 is required but found " 10 | f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}" 11 | ) 12 | sys.exit(1) 13 | 14 | print( 15 | "Python version " 16 | f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}" 17 | " is compatible\n" 18 | ) 19 | -------------------------------------------------------------------------------- /scripts/choose_staking.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # ------------------------------------------------------------------------------ 4 | # 5 | # Copyright 2023-2024 Valory AG 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | # ------------------------------------------------------------------------------ 20 | 21 | """Choose staking program.""" 22 | 23 | import argparse 24 | import json 25 | import os 26 | import sys 27 | import textwrap 28 | from pathlib import Path 29 | from typing import Any, Dict, List 30 | 31 | import requests 32 | from dotenv import dotenv_values, set_key, unset_key 33 | from web3 import Web3 34 | 35 | 36 | SCRIPT_PATH = Path(__file__).resolve().parent 37 | STORE_PATH = Path(SCRIPT_PATH, "..", ".trader_runner") 38 | DOTENV_PATH = Path(STORE_PATH, ".env") 39 | RPC_PATH = Path(STORE_PATH, "rpc.txt") 40 | STAKING_TOKEN_INSTANCE_ABI_PATH = Path( 41 | SCRIPT_PATH, 42 | "..", 43 | "trader", 44 | "packages", 45 | "valory", 46 | "contracts", 47 | "staking_token", 48 | "build", 49 | "StakingToken.json", 50 | ) 51 | STAKING_TOKEN_IMPLEMENTATION_ABI_PATH = STAKING_TOKEN_INSTANCE_ABI_PATH 52 | ACTIVITY_CHECKER_ABI_PATH = Path( 53 | SCRIPT_PATH, 54 | "..", 55 | "trader", 56 | "packages", 57 | "valory", 58 | "contracts", 59 | "mech_activity", 60 | "build", 61 | "MechActivity.json", 62 | ) 63 | 64 | IPFS_ADDRESS = "https://gateway.autonolas.tech/ipfs/f01701220{hash}" 65 | NEVERMINED_MECH_CONTRACT_ADDRESS = "0x327E26bDF1CfEa50BFAe35643B23D5268E41F7F9" 66 | NEVERMINED_AGENT_REGISTRY_ADDRESS = "0xAed729d4f4b895d8ca84ba022675bB0C44d2cD52" 67 | NEVERMINED_MECH_REQUEST_PRICE = "0" 68 | ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" 69 | DEPRECATED_TEXT = "(DEPRECATED)" 70 | NO_STAKING_PROGRAM_ID = "no_staking" 71 | NO_STAKING_PROGRAM_METADATA = { 72 | "name": "No staking", 73 | "description": "Your Olas Predict agent will still actively participate in prediction\ 74 | markets, but it will not be staked within any staking program.", 75 | } 76 | NO_STAKING_PROGRAM_ENV_VARIABLES = { 77 | "USE_STAKING": "false", 78 | "STAKING_PROGRAM": NO_STAKING_PROGRAM_ID, 79 | "AGENT_ID": "25", 80 | "CUSTOM_SERVICE_REGISTRY_ADDRESS": "0x9338b5153AE39BB89f50468E608eD9d764B755fD", 81 | "CUSTOM_SERVICE_REGISTRY_TOKEN_UTILITY_ADDRESS": "0xa45E64d13A30a51b91ae0eb182e88a40e9b18eD8", 82 | "MECH_CONTRACT_ADDRESS": "0x77af31De935740567Cf4fF1986D04B2c964A786a", 83 | "CUSTOM_OLAS_ADDRESS": ZERO_ADDRESS, 84 | "CUSTOM_STAKING_ADDRESS": "0x43fB32f25dce34EB76c78C7A42C8F40F84BCD237", # Non-staking agents need to specify an arbitrary staking contract so that they can call getStakingState() 85 | "MECH_ACTIVITY_CHECKER_CONTRACT": ZERO_ADDRESS, 86 | "MIN_STAKING_BOND_OLAS": "0", 87 | "MIN_STAKING_DEPOSIT_OLAS": "0", 88 | } 89 | 90 | STAKING_PROGRAMS = { 91 | NO_STAKING_PROGRAM_ID: ZERO_ADDRESS, 92 | "quickstart_beta_hobbyist": "0x389B46c259631Acd6a69Bde8B6cEe218230bAE8C", 93 | "quickstart_beta_hobbyist_2": "0x238EB6993b90a978ec6AAD7530d6429c949C08DA", 94 | "quickstart_beta_expert": "0x5344B7DD311e5d3DdDd46A4f71481bD7b05AAA3e", 95 | "quickstart_beta_expert_2": "0xb964e44c126410df341ae04B13aB10A985fE3513", 96 | "quickstart_beta_expert_3": "0x80faD33Cadb5F53f9D29F02Db97D682E8b101618", 97 | "quickstart_beta_expert_4": "0xaD9d891134443B443D7F30013c7e14Fe27F2E029", 98 | "quickstart_beta_expert_5": "0xE56dF1E563De1B10715cB313D514af350D207212", 99 | "quickstart_beta_expert_6": "0x2546214aEE7eEa4bEE7689C81231017CA231Dc93", 100 | "quickstart_beta_expert_7": "0xD7A3C8b975f71030135f1a66e9e23164d54fF455", 101 | "quickstart_beta_expert_8": "0x356C108D49C5eebd21c84c04E9162de41933030c", 102 | "quickstart_beta_expert_9": "0x17dBAe44BC5618Cc254055b386A29576b4F87015", 103 | "quickstart_beta_expert_10": "0xB0ef657b8302bd2c74B6E6D9B2b4b39145b19c6f", 104 | "quickstart_beta_expert_11": "0x3112c1613eAC3dBAE3D4E38CeF023eb9E2C91CF7", 105 | "quickstart_beta_expert_12": "0xF4a75F476801B3fBB2e7093aCDcc3576593Cc1fc", 106 | } 107 | 108 | DEPRECATED_STAKING_PROGRAMS = { 109 | "quickstart_alpha_everest": "0x5add592ce0a1B5DceCebB5Dcac086Cd9F9e3eA5C", 110 | "quickstart_alpha_alpine": "0x2Ef503950Be67a98746F484DA0bBAdA339DF3326", 111 | "quickstart_alpha_coastal": "0x43fB32f25dce34EB76c78C7A42C8F40F84BCD237", 112 | } 113 | 114 | 115 | def _prompt_select_staking_program() -> str: 116 | env_file_vars = dotenv_values(DOTENV_PATH) 117 | 118 | program_id = None 119 | if "STAKING_PROGRAM" in env_file_vars: 120 | print("The staking program is already selected.") 121 | 122 | program_id = env_file_vars.get("STAKING_PROGRAM") 123 | if program_id not in STAKING_PROGRAMS: 124 | print(f"WARNING: Selected staking program {program_id} is unknown.") 125 | print("") 126 | program_id = None 127 | 128 | if not program_id: 129 | if os.environ.get("ATTENDED") == "false": 130 | print( 131 | "No staking program set in environment variable STAKING_PROGRAM. Defaulting to 'no_staking'." 132 | ) 133 | return NO_STAKING_PROGRAM_ID 134 | 135 | print("Please, select your staking program preference") 136 | print("----------------------------------------------") 137 | ids = list(STAKING_PROGRAMS.keys()) 138 | for index, key in enumerate(ids): 139 | metadata = _get_staking_contract_metadata(program_id=key) 140 | name = metadata["name"] 141 | description = metadata["description"] 142 | wrapped_description = textwrap.fill( 143 | description, width=80, initial_indent=" ", subsequent_indent=" " 144 | ) 145 | print(f"{index + 1}) {name}\n{wrapped_description}\n") 146 | 147 | while True: 148 | try: 149 | choice = int(input(f"Enter your choice (1 - {len(ids)}): ")) - 1 150 | if not (0 <= choice < len(ids)): 151 | raise ValueError 152 | program_id = ids[choice] 153 | break 154 | except ValueError: 155 | print(f"Please enter a valid option (1 - {len(ids)}).") 156 | 157 | print(f"Selected staking program: {program_id}") 158 | print("") 159 | return program_id 160 | 161 | 162 | def _get_abi(contract_address: str) -> List: 163 | contract_abi_url = ( 164 | "https://gnosis.blockscout.com/api/v2/smart-contracts/{contract_address}" 165 | ) 166 | response = requests.get( 167 | contract_abi_url.format(contract_address=contract_address) 168 | ).json() 169 | 170 | if "result" in response: 171 | result = response["result"] 172 | try: 173 | abi = json.loads(result) 174 | except json.JSONDecodeError: 175 | print("Error: Failed to parse 'result' field as JSON") 176 | sys.exit(1) 177 | else: 178 | abi = response.get("abi") 179 | 180 | return abi if abi else [] 181 | 182 | 183 | def _load_abi_from_file(path: Path) -> Dict[str, Any]: 184 | if not os.path.exists(path): 185 | print( 186 | "Error: Contract airtfacts not found. Please execute 'run_service.sh' before executing this script." 187 | ) 188 | sys.exit(1) 189 | 190 | with open(path, "r", encoding="utf-8") as f: 191 | data = json.load(f) 192 | 193 | return data.get("abi") 194 | 195 | 196 | contracts_cache: Dict[str, Any] = {} 197 | 198 | 199 | def _get_staking_token_contract(program_id: str, use_blockscout: bool = False) -> Any: 200 | if program_id in contracts_cache: 201 | return contracts_cache[program_id] 202 | 203 | with open(RPC_PATH, "r", encoding="utf-8") as file: 204 | rpc = file.read().strip() 205 | 206 | w3 = Web3(Web3.HTTPProvider(rpc)) 207 | staking_token_instance_address = STAKING_PROGRAMS.get(program_id) 208 | if use_blockscout: 209 | abi = _get_abi(staking_token_instance_address) 210 | else: 211 | abi = _load_abi_from_file(STAKING_TOKEN_INSTANCE_ABI_PATH) 212 | contract = w3.eth.contract(address=staking_token_instance_address, abi=abi) 213 | 214 | if "getImplementation" in [func.fn_name for func in contract.all_functions()]: 215 | # It is a proxy contract 216 | implementation_address = contract.functions.getImplementation().call() 217 | if use_blockscout: 218 | abi = _get_abi(implementation_address) 219 | else: 220 | abi = _load_abi_from_file(STAKING_TOKEN_IMPLEMENTATION_ABI_PATH) 221 | contract = w3.eth.contract(address=staking_token_instance_address, abi=abi) 222 | 223 | contracts_cache[program_id] = contract 224 | return contract 225 | 226 | 227 | def _get_staking_contract_metadata( 228 | program_id: str, use_blockscout: bool = False 229 | ) -> Dict[str, str]: 230 | try: 231 | if program_id == NO_STAKING_PROGRAM_ID: 232 | return NO_STAKING_PROGRAM_METADATA 233 | 234 | staking_token_contract = _get_staking_token_contract( 235 | program_id=program_id, use_blockscout=use_blockscout 236 | ) 237 | metadata_hash = staking_token_contract.functions.metadataHash().call() 238 | ipfs_address = IPFS_ADDRESS.format(hash=metadata_hash.hex()) 239 | response = requests.get(ipfs_address) 240 | 241 | if response.status_code == 200: 242 | return response.json() 243 | 244 | raise Exception( # pylint: disable=broad-except 245 | f"Failed to fetch data from {ipfs_address}: {response.status_code}" 246 | ) 247 | except Exception: # pylint: disable=broad-except 248 | return { 249 | "name": program_id, 250 | "description": program_id, 251 | } 252 | 253 | 254 | def _get_staking_env_variables( # pylint: disable=too-many-locals 255 | program_id: str, use_blockscout: bool = False 256 | ) -> Dict[str, str]: 257 | if program_id == NO_STAKING_PROGRAM_ID: 258 | return NO_STAKING_PROGRAM_ENV_VARIABLES 259 | 260 | staking_token_instance_address = STAKING_PROGRAMS.get(program_id) 261 | staking_token_contract = _get_staking_token_contract( 262 | program_id=program_id, use_blockscout=use_blockscout 263 | ) 264 | agent_id = staking_token_contract.functions.agentIds(0).call() 265 | service_registry = staking_token_contract.functions.serviceRegistry().call() 266 | staking_token = staking_token_contract.functions.stakingToken().call() 267 | service_registry_token_utility = ( 268 | staking_token_contract.functions.serviceRegistryTokenUtility().call() 269 | ) 270 | min_staking_deposit = staking_token_contract.functions.minStakingDeposit().call() 271 | min_staking_bond = min_staking_deposit 272 | 273 | if "activityChecker" in [ 274 | func.fn_name for func in staking_token_contract.all_functions() 275 | ]: 276 | activity_checker = staking_token_contract.functions.activityChecker().call() 277 | 278 | if use_blockscout: 279 | abi = _get_abi(activity_checker) 280 | else: 281 | abi = _load_abi_from_file(ACTIVITY_CHECKER_ABI_PATH) 282 | 283 | with open(RPC_PATH, "r", encoding="utf-8") as file: 284 | rpc = file.read().strip() 285 | 286 | w3 = Web3(Web3.HTTPProvider(rpc)) 287 | activity_checker_contract = w3.eth.contract(address=activity_checker, abi=abi) 288 | agent_mech = activity_checker_contract.functions.agentMech().call() 289 | else: 290 | activity_checker = ZERO_ADDRESS 291 | agent_mech = staking_token_contract.functions.agentMech().call() 292 | 293 | return { 294 | "USE_STAKING": "true", 295 | "STAKING_PROGRAM": program_id, 296 | "AGENT_ID": agent_id, 297 | "CUSTOM_SERVICE_REGISTRY_ADDRESS": service_registry, 298 | "CUSTOM_SERVICE_REGISTRY_TOKEN_UTILITY_ADDRESS": service_registry_token_utility, 299 | "CUSTOM_OLAS_ADDRESS": staking_token, 300 | "CUSTOM_STAKING_ADDRESS": staking_token_instance_address, 301 | "MECH_ACTIVITY_CHECKER_CONTRACT": activity_checker, 302 | "MECH_CONTRACT_ADDRESS": agent_mech, 303 | "MIN_STAKING_BOND_OLAS": min_staking_bond, 304 | "MIN_STAKING_DEPOSIT_OLAS": min_staking_deposit, 305 | } 306 | 307 | 308 | def _set_dotenv_file_variables(env_vars: Dict[str, str]) -> None: 309 | for key, value in env_vars.items(): 310 | if value: 311 | set_key( 312 | dotenv_path=DOTENV_PATH, 313 | key_to_set=key, 314 | value_to_set=value, 315 | quote_mode="never", 316 | ) 317 | else: 318 | unset_key(dotenv_path=DOTENV_PATH, key_to_unset=key) 319 | 320 | 321 | def _get_nevermined_env_variables() -> Dict[str, str]: 322 | env_file_vars = dotenv_values(DOTENV_PATH) 323 | use_nevermined = False 324 | 325 | if "USE_NEVERMINED" not in env_file_vars: 326 | set_key( 327 | dotenv_path=DOTENV_PATH, 328 | key_to_set="USE_NEVERMINED", 329 | value_to_set="false", 330 | quote_mode="never", 331 | ) 332 | elif env_file_vars.get("USE_NEVERMINED").strip() not in ("True", "true"): 333 | set_key( 334 | dotenv_path=DOTENV_PATH, 335 | key_to_set="USE_NEVERMINED", 336 | value_to_set="false", 337 | quote_mode="never", 338 | ) 339 | else: 340 | use_nevermined = True 341 | 342 | if use_nevermined: 343 | print( 344 | " - A Nevermined subscription will be used to pay for the mech requests." 345 | ) 346 | return { 347 | "MECH_CONTRACT_ADDRESS": NEVERMINED_MECH_CONTRACT_ADDRESS, 348 | "AGENT_REGISTRY_ADDRESS": NEVERMINED_AGENT_REGISTRY_ADDRESS, 349 | "MECH_REQUEST_PRICE": NEVERMINED_MECH_REQUEST_PRICE, 350 | } 351 | else: 352 | print(" - No Nevermined subscription set.") 353 | return {"AGENT_REGISTRY_ADDRESS": "", "MECH_REQUEST_PRICE": ""} 354 | 355 | 356 | def main() -> None: 357 | """Main method""" 358 | parser = argparse.ArgumentParser(description="Set up staking configuration.") 359 | parser.add_argument( 360 | "--reset", 361 | action="store_true", 362 | help="Reset USE_STAKING and STAKING_PROGRAM in .env file", 363 | ) 364 | parser.add_argument( 365 | "--use_blockscout", 366 | action="store_true", 367 | help="Use Blockscout to retrieve contract data.", 368 | ) 369 | args = parser.parse_args() 370 | 371 | if args.reset: 372 | env_file_vars = dotenv_values(DOTENV_PATH) 373 | staking_program = env_file_vars.get("STAKING_PROGRAM") 374 | print("=====================================") 375 | print("Reset your staking program preference") 376 | print("=====================================") 377 | print("") 378 | print(f"Your current staking program preference is set to '{staking_program}'.") 379 | print( 380 | "You can reset your preference. However, your trader might not be able to switch between staking contracts until it has been staked for a minimum staking period in the current program." 381 | ) 382 | print("") 383 | if os.environ.get("ATTENDED") == "true": 384 | response = ( 385 | input( 386 | "Do you want to reset your staking program preference? (yes/no): " 387 | ) 388 | .strip() 389 | .lower() 390 | ) 391 | if response not in ["yes", "y"]: 392 | return 393 | 394 | print("") 395 | unset_key(dotenv_path=DOTENV_PATH, key_to_unset="USE_STAKING") 396 | unset_key(dotenv_path=DOTENV_PATH, key_to_unset="STAKING_PROGRAM") 397 | print( 398 | f"Environment variables USE_STAKING and STAKING_PROGRAM have been reset in '{DOTENV_PATH}'." 399 | ) 400 | print("") 401 | 402 | program_id = _prompt_select_staking_program() 403 | 404 | print(" - Populating staking program variables in the .env file") 405 | staking_env_variables = _get_staking_env_variables( 406 | program_id, use_blockscout=args.use_blockscout 407 | ) 408 | _set_dotenv_file_variables(staking_env_variables) 409 | 410 | print(" - Populating Nevermined variables in the .env file") 411 | print("") 412 | nevermined_env_variables = _get_nevermined_env_variables() 413 | _set_dotenv_file_variables(nevermined_env_variables) 414 | print("") 415 | print("Finished populating the .env file.") 416 | 417 | 418 | if __name__ == "__main__": 419 | main() 420 | -------------------------------------------------------------------------------- /scripts/claim_staking_rewards.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # ------------------------------------------------------------------------------ 4 | # 5 | # Copyright 2024 Valory AG 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | # ------------------------------------------------------------------------------ 20 | 21 | """Claim earned OLAS""" 22 | 23 | import json 24 | import os 25 | import sys 26 | from getpass import getpass 27 | from pathlib import Path 28 | from typing import Any, Dict, Optional 29 | 30 | from aea.crypto.helpers import DecryptError, KeyIsIncorrect 31 | from aea_ledger_ethereum.ethereum import EthereumCrypto 32 | from dotenv import dotenv_values 33 | from web3 import Web3 34 | 35 | 36 | SCRIPT_PATH = Path(__file__).resolve().parent 37 | STORE_PATH = Path(SCRIPT_PATH, "..", ".trader_runner") 38 | DOTENV_PATH = Path(STORE_PATH, ".env") 39 | RPC_PATH = Path(STORE_PATH, "rpc.txt") 40 | SERVICE_ID_PATH = Path(STORE_PATH, "service_id.txt") 41 | SERVICE_SAFE_ADDRESS_PATH = Path(STORE_PATH, "service_safe_address.txt") 42 | OPERATOR_PKEY_PATH = Path(STORE_PATH, "operator_pkey.txt") 43 | DEFAULT_ENCODING = "utf-8" 44 | 45 | OLAS_TOKEN_ADDRESS_GNOSIS = "0xcE11e14225575945b8E6Dc0D4F2dD4C570f79d9f" 46 | GNOSIS_CHAIN_ID = 100 47 | DEFAULT_GAS = 100000 48 | SAFE_WEBAPP_URL = "https://app.safe.global/home?safe=gno:" 49 | 50 | STAKING_TOKEN_INSTANCE_ABI_PATH = Path( 51 | SCRIPT_PATH, 52 | "..", 53 | "trader", 54 | "packages", 55 | "valory", 56 | "contracts", 57 | "staking_token", 58 | "build", 59 | "StakingToken.json", 60 | ) 61 | STAKING_TOKEN_IMPLEMENTATION_ABI_PATH = STAKING_TOKEN_INSTANCE_ABI_PATH 62 | 63 | ERC20_ABI_PATH = Path( 64 | SCRIPT_PATH, 65 | "..", 66 | "trader", 67 | "packages", 68 | "valory", 69 | "contracts", 70 | "erc20", 71 | "build", 72 | "ERC20.json", 73 | ) 74 | 75 | 76 | def _is_keystore(pkeypath: Path) -> bool: 77 | try: 78 | with open(pkeypath, "r", encoding="utf-8") as f: 79 | json.load(f) 80 | return True 81 | except json.JSONDecodeError: 82 | return False 83 | 84 | 85 | def _load_abi_from_file(path: Path) -> Dict[str, Any]: 86 | if not os.path.exists(path): 87 | print( 88 | "Error: Contract airtfacts not found. Please execute 'run_service.sh' before executing this script." 89 | ) 90 | sys.exit(1) 91 | 92 | with open(path, "r", encoding=DEFAULT_ENCODING) as f: 93 | data = json.load(f) 94 | 95 | return data.get("abi") 96 | 97 | 98 | def _erc20_balance( 99 | address: str, 100 | token_address: str = OLAS_TOKEN_ADDRESS_GNOSIS, 101 | token_name: str = "OLAS", 102 | decimal_precision: int = 2, 103 | ) -> str: 104 | """Get ERC20 balance""" 105 | rpc = RPC_PATH.read_text(encoding=DEFAULT_ENCODING).strip() 106 | w3 = Web3(Web3.HTTPProvider(rpc)) 107 | abi = _load_abi_from_file(ERC20_ABI_PATH) 108 | contract = w3.eth.contract(address=token_address, abi=abi) 109 | balance = contract.functions.balanceOf(address).call() 110 | return f"{balance / 10**18:.{decimal_precision}f} {token_name}" 111 | 112 | 113 | def _claim_rewards( # pylint: disable=too-many-locals 114 | password: Optional[str] = None, 115 | ) -> None: 116 | service_safe_address = SERVICE_SAFE_ADDRESS_PATH.read_text( 117 | encoding=DEFAULT_ENCODING 118 | ).strip() 119 | print( 120 | f"OLAS Balance of service Safe {service_safe_address}: {_erc20_balance(service_safe_address)}" 121 | ) 122 | 123 | env_file_vars = dotenv_values(DOTENV_PATH) 124 | staking_token_address = env_file_vars["CUSTOM_STAKING_ADDRESS"] 125 | service_id = int(SERVICE_ID_PATH.read_text(encoding=DEFAULT_ENCODING).strip()) 126 | 127 | rpc = RPC_PATH.read_text(encoding=DEFAULT_ENCODING).strip() 128 | w3 = Web3(Web3.HTTPProvider(rpc)) 129 | abi = _load_abi_from_file(STAKING_TOKEN_IMPLEMENTATION_ABI_PATH) 130 | staking_token_contract = w3.eth.contract(address=staking_token_address, abi=abi) 131 | 132 | try: 133 | ethereum_crypto = EthereumCrypto(OPERATOR_PKEY_PATH, password=password) 134 | operator_address = ethereum_crypto.address 135 | operator_pkey = ethereum_crypto.private_key 136 | except DecryptError: 137 | print( 138 | f"Could not decrypt key {OPERATOR_PKEY_PATH}. Please verify if your key file is password-protected, and if the provided password is correct (passwords are case-sensitive)." 139 | ) 140 | sys.exit(1) 141 | except KeyIsIncorrect: 142 | print(f"Error decoding key file {OPERATOR_PKEY_PATH}.") 143 | sys.exit(1) 144 | 145 | function = staking_token_contract.functions.claim(service_id) 146 | claim_transaction = function.build_transaction( 147 | { 148 | "chainId": GNOSIS_CHAIN_ID, 149 | "gas": DEFAULT_GAS, 150 | "gasPrice": w3.to_wei("3", "gwei"), 151 | "nonce": w3.eth.get_transaction_count(operator_address), 152 | } 153 | ) 154 | 155 | signed_tx = w3.eth.account.sign_transaction(claim_transaction, operator_pkey) 156 | tx_hash = w3.eth.send_raw_transaction(signed_tx.rawTransaction) 157 | tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash) 158 | print(f"Claim transaction done. Hash: {tx_hash.hex()}") 159 | 160 | if "status" in tx_receipt and tx_receipt["status"] == 0: 161 | print( 162 | "WARNING: The transaction was reverted. This may be caused because your service does not have rewards to claim." 163 | ) 164 | else: 165 | print("") 166 | print(f"Claimed OLAS transferred to your service Safe {service_safe_address}") 167 | 168 | print("") 169 | print( 170 | f"You can use your Owner/Operator wallet (address {operator_address}) to connect your Safe at {SAFE_WEBAPP_URL}{service_safe_address}." 171 | ) 172 | 173 | 174 | def main() -> None: 175 | """Main method.""" 176 | print("---------------------") 177 | print("Claim staking rewards") 178 | print("---------------------") 179 | print("") 180 | print( 181 | "This script will claim the OLAS staking rewards accrued in the current staking contract and transfer them to your service Safe." 182 | ) 183 | _continue = input("Do you want to continue (yes/no)? ").strip().lower() 184 | 185 | if _continue not in ("y", "yes"): 186 | sys.exit(0) 187 | 188 | print("") 189 | 190 | password = None 191 | if _is_keystore(OPERATOR_PKEY_PATH): 192 | print("Enter your password") 193 | print("-------------------") 194 | print("Your key files are protected with a password.") 195 | password = getpass("Please, enter your password: ").strip() 196 | print("") 197 | 198 | _claim_rewards(password) 199 | 200 | 201 | if __name__ == "__main__": 202 | main() 203 | -------------------------------------------------------------------------------- /scripts/erc20_balance.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # ------------------------------------------------------------------------------ 4 | # 5 | # Copyright 2022-2023 Valory AG 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | # ------------------------------------------------------------------------------ 20 | 21 | """This script prints the wxDAI balance of an address in WEI.""" 22 | 23 | import json 24 | import sys 25 | 26 | from web3 import Web3, HTTPProvider 27 | 28 | WXDAI_CONTRACT_ADDRESS = "0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d" 29 | WXDAI_ABI_PATH = "../trader/packages/valory/contracts/erc20/build/ERC20.json" 30 | 31 | 32 | def get_balance() -> int: 33 | """Get the wxDAI balance of an address in WEI.""" 34 | w3 = Web3(HTTPProvider(rpc)) 35 | contract_instance = w3.eth.contract(address=token, abi=abi) 36 | return contract_instance.functions.balanceOf(w3.to_checksum_address(address)).call() 37 | 38 | 39 | def read_abi() -> str: 40 | """Read and return the wxDAI contract's ABI.""" 41 | with open(WXDAI_ABI_PATH) as f: 42 | data = json.loads(f.read()) 43 | 44 | return data.get('abi', []) 45 | 46 | 47 | if __name__ == "__main__": 48 | if len(sys.argv) != 4: 49 | raise ValueError("Expected the address and the rpc as positional arguments.") 50 | else: 51 | token = sys.argv[1] 52 | address = sys.argv[2] 53 | rpc = sys.argv[3] 54 | abi = read_abi() 55 | print(get_balance()) 56 | -------------------------------------------------------------------------------- /scripts/get_agent_bond.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # ------------------------------------------------------------------------------ 4 | # 5 | # Copyright 2022-2023 Valory AG 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | # ------------------------------------------------------------------------------ 20 | 21 | """Get agent bond.""" 22 | 23 | import argparse 24 | import json 25 | import os 26 | import sys 27 | from pathlib import Path 28 | from typing import Any, Dict, List 29 | 30 | import requests 31 | from web3 import Web3 32 | 33 | 34 | ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" 35 | SCRIPT_PATH = Path(__file__).resolve().parent 36 | SERVICE_REGISTRY_TOKEN_UTILITY_ABI_PATH = Path( 37 | SCRIPT_PATH, "..", "contracts", "ServiceRegistryTokenUtility.json" 38 | ) 39 | 40 | 41 | def _get_abi(contract_address: str) -> List: 42 | contract_abi_url = ( 43 | "https://gnosis.blockscout.com/api/v2/smart-contracts/{contract_address}" 44 | ) 45 | response = requests.get( 46 | contract_abi_url.format(contract_address=contract_address) 47 | ).json() 48 | 49 | if "result" in response: 50 | result = response["result"] 51 | try: 52 | abi = json.loads(result) 53 | except json.JSONDecodeError: 54 | print("Error: Failed to parse 'result' field as JSON") 55 | sys.exit(1) 56 | else: 57 | abi = response.get("abi") 58 | 59 | return abi if abi else [] 60 | 61 | 62 | def _load_abi_from_file(path: Path) -> Dict[str, Any]: 63 | if not os.path.exists(path): 64 | print( 65 | "Error: Contract airtfacts not found. Please execute 'run_service.sh' before executing this script." 66 | ) 67 | sys.exit(1) 68 | 69 | with open(path, "r", encoding="utf-8") as f: 70 | data = json.load(f) 71 | 72 | return data.get("abi") 73 | 74 | 75 | def main() -> None: 76 | """Main method""" 77 | parser = argparse.ArgumentParser( 78 | description="Get agent bond from service registry token utility contract." 79 | ) 80 | parser.add_argument( 81 | "service_registry", type=str, help="Service registry contract address" 82 | ) 83 | parser.add_argument( 84 | "service_registry_token_utility", 85 | type=str, 86 | help="Service registry token utility contract address", 87 | ) 88 | parser.add_argument("service_id", type=int, help="Service ID") 89 | parser.add_argument("agent_id", type=int, help="Agent ID") 90 | parser.add_argument("rpc", type=str, help="RPC") 91 | parser.add_argument( 92 | "--use_blockscout", 93 | action="store_true", 94 | help="Use Blockscout to retrieve contract data.", 95 | ) 96 | args = parser.parse_args() 97 | 98 | service_registry = args.service_registry 99 | service_registry_token_utility = args.service_registry_token_utility 100 | service_id = args.service_id 101 | agent_id = args.agent_id 102 | rpc = args.rpc 103 | 104 | w3 = Web3(Web3.HTTPProvider(rpc)) 105 | 106 | if args.use_blockscout: 107 | abi = _get_abi(service_registry_token_utility) 108 | else: 109 | abi = _load_abi_from_file(SERVICE_REGISTRY_TOKEN_UTILITY_ABI_PATH) 110 | 111 | contract = w3.eth.contract(address=service_registry_token_utility, abi=abi) 112 | token = contract.functions.mapServiceIdTokenDeposit(service_id).call()[0] 113 | 114 | # If service is token-secured, retrieve bond from Service Registry Token Utility 115 | if token != ZERO_ADDRESS: 116 | agent_bond = contract.functions.getAgentBond(service_id, agent_id).call() 117 | print(agent_bond) 118 | # Otherwise, retrieve bond from Service Registry 119 | else: 120 | abi = _get_abi(service_registry) 121 | contract = w3.eth.contract(address=service_registry, abi=abi) 122 | agent_bond = contract.functions.getService(service_id).call()[0] 123 | print(agent_bond) 124 | 125 | 126 | if __name__ == "__main__": 127 | main() 128 | -------------------------------------------------------------------------------- /scripts/get_available_staking_slots.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # ------------------------------------------------------------------------------ 4 | # 5 | # Copyright 2022-2024 Valory AG 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | # ------------------------------------------------------------------------------ 20 | 21 | """Get the available staking slots.""" 22 | 23 | import argparse 24 | import sys 25 | import traceback 26 | import typing 27 | from pathlib import Path 28 | 29 | from aea_ledger_ethereum.ethereum import EthereumApi, EthereumCrypto 30 | from utils import get_available_staking_slots 31 | 32 | 33 | if __name__ == "__main__": 34 | try: 35 | parser = argparse.ArgumentParser( 36 | description="Get the available staking slots." 37 | ) 38 | parser.add_argument( 39 | "staking_contract_address", 40 | type=str, 41 | help="The staking contract address.", 42 | ) 43 | parser.add_argument("rpc", type=str, help="RPC for the Gnosis chain") 44 | args = parser.parse_args() 45 | 46 | ledger_api = EthereumApi(address=args.rpc) 47 | available_staking_slots = get_available_staking_slots( 48 | ledger_api, args.staking_contract_address 49 | ) 50 | 51 | print(available_staking_slots) 52 | 53 | except Exception as e: # pylint: disable=broad-except 54 | print(f"An error occurred while executing {Path(__file__).name}: {str(e)}") 55 | traceback.print_exc() 56 | sys.exit(1) 57 | -------------------------------------------------------------------------------- /scripts/get_safe_owners.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # ------------------------------------------------------------------------------ 4 | # 5 | # Copyright 2022-2023 Valory AG 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | # ------------------------------------------------------------------------------ 20 | 21 | """Get a Safe current owners' addresses.""" 22 | 23 | import argparse 24 | import sys 25 | import traceback 26 | import typing 27 | from pathlib import Path 28 | 29 | from aea.contracts.base import Contract 30 | from aea_ledger_ethereum.ethereum import EthereumApi, EthereumCrypto 31 | 32 | from packages.valory.contracts.gnosis_safe.contract import GnosisSafeContract 33 | 34 | 35 | ContractType = typing.TypeVar("ContractType") 36 | 37 | 38 | def load_contract(ctype: ContractType) -> ContractType: 39 | """Load contract.""" 40 | *parts, _ = ctype.__module__.split(".") 41 | path = "/".join(parts) 42 | return Contract.from_dir(directory=path) 43 | 44 | 45 | if __name__ == "__main__": 46 | try: 47 | parser = argparse.ArgumentParser( 48 | description="Get a Safe current owners' addresses." 49 | ) 50 | parser.add_argument( 51 | "safe_address", 52 | type=str, 53 | help="Safe address", 54 | ) 55 | parser.add_argument( 56 | "private_key_path", 57 | type=str, 58 | help="Path to the file containing the Ethereum private key", 59 | ) 60 | parser.add_argument("rpc", type=str, help="RPC for the Gnosis chain") 61 | parser.add_argument("--password", type=str, help="Private key password") 62 | args = parser.parse_args() 63 | 64 | ledger_api = EthereumApi(address=args.rpc) 65 | ethereum_crypto: EthereumCrypto 66 | ethereum_crypto = EthereumCrypto( 67 | private_key_path=args.private_key_path, password=args.password 68 | ) 69 | 70 | safe = load_contract(GnosisSafeContract) 71 | print( 72 | safe.get_owners( 73 | ledger_api=ledger_api, contract_address=args.safe_address 74 | ).get("owners", []) 75 | ) 76 | 77 | except Exception as e: # pylint: disable=broad-except 78 | print(f"An error occurred while executing {Path(__file__).name}: {str(e)}") 79 | traceback.print_exc() 80 | sys.exit(1) 81 | -------------------------------------------------------------------------------- /scripts/is_keys_json_password_valid.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # ------------------------------------------------------------------------------ 4 | # 5 | # Copyright 2022-2023 Valory AG 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | # ------------------------------------------------------------------------------ 20 | 21 | """Checks if the provided password is valid for a keys.json file.""" 22 | 23 | import argparse 24 | import json 25 | import tempfile 26 | import traceback 27 | from pathlib import Path 28 | 29 | from aea.crypto.helpers import DecryptError, KeyIsIncorrect 30 | from aea_ledger_ethereum.ethereum import EthereumCrypto 31 | 32 | 33 | def _is_keys_json_password_valid( 34 | keys_json_path: Path, password: str, debug: bool 35 | ) -> bool: 36 | keys = json.load(keys_json_path.open("r")) 37 | 38 | with tempfile.TemporaryDirectory() as temp_dir: 39 | for idx, key in enumerate(keys): 40 | temp_file = Path(temp_dir, str(idx)) 41 | temp_file.open("w+", encoding="utf-8").write(str(key["private_key"])) 42 | 43 | try: 44 | EthereumCrypto.load_private_key_from_path( 45 | str(temp_file), password=password 46 | ) 47 | except ( 48 | DecryptError, 49 | json.decoder.JSONDecodeError, 50 | KeyIsIncorrect, 51 | ): 52 | if debug: 53 | stack_trace = traceback.format_exc() 54 | print(stack_trace) 55 | return False 56 | 57 | return True 58 | 59 | 60 | if __name__ == "__main__": 61 | parser = argparse.ArgumentParser( 62 | description="Checks if the provided password is valid for a keys.json file." 63 | ) 64 | parser.add_argument("keys_json_path", type=str, help="Path to the keys.json file.") 65 | parser.add_argument( 66 | "--password", 67 | type=str, 68 | help="Password. If not provided, it assumes keys.json is not password-protected.", 69 | ) 70 | parser.add_argument("--debug", action="store_true", help="Prints debug messages.") 71 | args = parser.parse_args() 72 | print( 73 | _is_keys_json_password_valid( 74 | Path(args.keys_json_path), args.password, args.debug 75 | ) 76 | ) 77 | -------------------------------------------------------------------------------- /scripts/mech_events.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # ------------------------------------------------------------------------------ 4 | # 5 | # Copyright 2022-2023 Valory AG 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | # ------------------------------------------------------------------------------ 20 | 21 | """Utilities to retrieve on-chain Mech events.""" 22 | 23 | import json 24 | import os 25 | import sys 26 | import time 27 | from dataclasses import dataclass 28 | from pathlib import Path 29 | from string import Template 30 | from typing import Any, ClassVar, Dict 31 | 32 | import requests 33 | from dotenv import dotenv_values 34 | from gql import Client, gql 35 | from gql.transport.requests import RequestsHTTPTransport 36 | from tqdm import tqdm 37 | from web3.datastructures import AttributeDict 38 | 39 | 40 | SCRIPT_PATH = Path(__file__).resolve().parent 41 | STORE_PATH = Path(SCRIPT_PATH, "..", ".trader_runner") 42 | ENV_FILENAME = ".env" 43 | DOTENV_PATH = STORE_PATH / ENV_FILENAME 44 | MECH_EVENTS_JSON_PATH = STORE_PATH / "mech_events.json" 45 | HTTP = "http://" 46 | HTTPS = HTTP[:4] + "s" + HTTP[4:] 47 | CID_PREFIX = "f01701220" 48 | IPFS_ADDRESS = f"{HTTPS}gateway.autonolas.tech/ipfs/" 49 | MECH_EVENTS_DB_VERSION = 3 50 | DEFAULT_MECH_FEE = 10000000000000000 51 | DEFAULT_FROM_TIMESTAMP = 0 52 | DEFAULT_TO_TIMESTAMP = 2147483647 53 | MECH_SUBGRAPH_URL_TEMPLATE = Template( 54 | "https://gateway.thegraph.com/api/${SUBGRAPH_API_KEY}/subgraphs/id/4YGoX3iXUni1NBhWJS5xyKcntrAzssfytJK7PQxxQk5g" 55 | ) 56 | SUBGRAPH_HEADERS = { 57 | "Accept": "application/json, multipart/mixed", 58 | "Content-Type": "application/json", 59 | } 60 | QUERY_BATCH_SIZE = 1000 61 | MECH_EVENTS_SUBGRAPH_QUERY_TEMPLATE = Template( 62 | """ 63 | query mech_events_subgraph_query($sender: Bytes, $id_gt: Bytes, $first: Int) { 64 | ${subgraph_event_set_name}( 65 | where: {sender: $sender, id_gt: $id_gt} 66 | first: $first 67 | orderBy: id 68 | orderDirection: asc 69 | ) { 70 | id 71 | ipfsHash 72 | requestId 73 | sender 74 | transactionHash 75 | blockNumber 76 | blockTimestamp 77 | } 78 | } 79 | """ 80 | ) 81 | 82 | 83 | @dataclass 84 | class MechBaseEvent: # pylint: disable=too-many-instance-attributes 85 | """Base class for mech's on-chain event representation.""" 86 | 87 | event_id: str 88 | sender: str 89 | transaction_hash: str 90 | ipfs_hash: str 91 | block_number: int 92 | block_timestamp: int 93 | ipfs_link: str 94 | ipfs_contents: Dict[str, Any] 95 | 96 | event_name: ClassVar[str] 97 | subgraph_event_name: ClassVar[str] 98 | 99 | def __init__( 100 | self, 101 | event_id: str, 102 | sender: str, 103 | ipfs_hash: str, 104 | transaction_hash: str, 105 | block_number: int, 106 | block_timestamp: int, 107 | ): # pylint: disable=too-many-arguments 108 | """Initializes the MechBaseEvent""" 109 | self.event_id = event_id 110 | self.sender = sender 111 | self.ipfs_hash = ipfs_hash 112 | self.transaction_hash = transaction_hash 113 | self.block_number = block_number 114 | self.block_timestamp = block_timestamp 115 | self.ipfs_link = "" 116 | self.ipfs_contents = {} 117 | self._populate_ipfs_contents(ipfs_hash) 118 | 119 | def _populate_ipfs_contents(self, data: str) -> None: 120 | url = f"{IPFS_ADDRESS}{data}" 121 | for _url in [f"{url}/metadata.json", url]: 122 | try: 123 | response = requests.get(_url) 124 | response.raise_for_status() 125 | self.ipfs_contents = response.json() 126 | self.ipfs_link = _url 127 | except Exception: # pylint: disable=broad-except 128 | continue 129 | 130 | 131 | @dataclass 132 | class MechRequest(MechBaseEvent): 133 | """A mech's on-chain response representation.""" 134 | 135 | request_id: str 136 | fee: int 137 | 138 | event_name: ClassVar[str] = "Request" 139 | subgraph_event_name: ClassVar[str] = "request" 140 | 141 | def __init__(self, event: AttributeDict): 142 | """Initializes the MechRequest""" 143 | 144 | super().__init__( 145 | event_id=event["requestId"], 146 | sender=event["sender"], 147 | ipfs_hash=event["ipfsHash"], 148 | transaction_hash=event["transactionHash"], 149 | block_number=int(event["blockNumber"]), 150 | block_timestamp=int(event["blockTimestamp"]), 151 | ) 152 | 153 | self.request_id = self.event_id 154 | # TODO This should be updated to extract the fee from the transaction. 155 | self.fee = DEFAULT_MECH_FEE 156 | 157 | 158 | def _read_mech_events_data_from_file() -> Dict[str, Any]: 159 | """Read Mech events data from the JSON file.""" 160 | try: 161 | with open(MECH_EVENTS_JSON_PATH, "r", encoding="utf-8") as file: 162 | mech_events_data = json.load(file) 163 | 164 | # Check if it is an old DB version 165 | if mech_events_data.get("db_version", 0) < MECH_EVENTS_DB_VERSION: 166 | current_time = time.strftime("%Y-%m-%d_%H-%M-%S") 167 | old_db_filename = f"mech_events.{current_time}.old.json" 168 | os.rename(MECH_EVENTS_JSON_PATH, STORE_PATH / old_db_filename) 169 | mech_events_data = {} 170 | mech_events_data["db_version"] = MECH_EVENTS_DB_VERSION 171 | except FileNotFoundError: 172 | mech_events_data = {} 173 | mech_events_data["db_version"] = MECH_EVENTS_DB_VERSION 174 | except json.decoder.JSONDecodeError: 175 | print( 176 | f'\nERROR: The local Mech events database "{MECH_EVENTS_JSON_PATH.resolve()}" is corrupted. Please try delete or rename the file, and run the script again.' 177 | ) 178 | sys.exit(1) 179 | 180 | return mech_events_data 181 | 182 | 183 | MINIMUM_WRITE_FILE_DELAY = 20 184 | last_write_time = 0.0 185 | 186 | 187 | def _write_mech_events_data_to_file( 188 | mech_events_data: Dict[str, Any], force_write: bool = False 189 | ) -> None: 190 | global last_write_time # pylint: disable=global-statement 191 | now = time.time() 192 | 193 | if force_write or (now - last_write_time) >= MINIMUM_WRITE_FILE_DELAY: 194 | with open(MECH_EVENTS_JSON_PATH, "w", encoding="utf-8") as file: 195 | json.dump(mech_events_data, file, indent=2) 196 | last_write_time = now 197 | 198 | 199 | def get_mech_subgraph_url() -> str: 200 | """Get the mech subgraph's URL.""" 201 | env_file_vars = dotenv_values(DOTENV_PATH) 202 | return MECH_SUBGRAPH_URL_TEMPLATE.substitute(env_file_vars) 203 | 204 | 205 | def _query_mech_events_subgraph( 206 | sender: str, event_cls: type[MechBaseEvent] 207 | ) -> dict[str, Any]: 208 | """Query the subgraph.""" 209 | 210 | mech_subgraph_url = get_mech_subgraph_url() 211 | transport = RequestsHTTPTransport(mech_subgraph_url) 212 | client = Client(transport=transport, fetch_schema_from_transport=True) 213 | 214 | subgraph_event_set_name = f"{event_cls.subgraph_event_name}s" 215 | all_results: dict[str, Any] = {"data": {subgraph_event_set_name: []}} 216 | query = MECH_EVENTS_SUBGRAPH_QUERY_TEMPLATE.safe_substitute( 217 | subgraph_event_set_name=subgraph_event_set_name 218 | ) 219 | id_gt = "" 220 | while True: 221 | variables = { 222 | "sender": sender, 223 | "id_gt": id_gt, 224 | "first": QUERY_BATCH_SIZE, 225 | } 226 | response = client.execute(gql(query), variable_values=variables) 227 | events = response.get(subgraph_event_set_name, []) 228 | 229 | if not events: 230 | break 231 | 232 | all_results["data"][subgraph_event_set_name].extend(events) 233 | id_gt = events[len(events) - 1]["id"] 234 | 235 | return all_results 236 | 237 | 238 | # pylint: disable=too-many-locals 239 | def _update_mech_events_db( 240 | sender: str, 241 | event_cls: type[MechBaseEvent], 242 | ) -> None: 243 | """Get the mech Events database.""" 244 | 245 | print( 246 | f"Updating the local Mech events database. This may take a while.\n" 247 | f" Event: {event_cls.event_name}\n" 248 | f" Sender address: {sender}" 249 | ) 250 | 251 | try: 252 | # Query the subgraph 253 | query = _query_mech_events_subgraph(sender, event_cls) 254 | subgraph_data = query["data"] 255 | 256 | # Read the current Mech events database 257 | mech_events_data = _read_mech_events_data_from_file() 258 | stored_events = mech_events_data.setdefault(sender, {}).setdefault( 259 | event_cls.event_name, {} 260 | ) 261 | 262 | subgraph_event_set_name = f"{event_cls.subgraph_event_name}s" 263 | for subgraph_event in tqdm( 264 | subgraph_data[subgraph_event_set_name], 265 | miniters=1, 266 | desc=" Processing", 267 | ): 268 | if subgraph_event[ 269 | "requestId" 270 | ] not in stored_events or not stored_events.get( 271 | subgraph_event["requestId"], {} 272 | ).get( 273 | "ipfs_contents" 274 | ): 275 | mech_event = event_cls(subgraph_event) # type: ignore 276 | stored_events[mech_event.event_id] = mech_event.__dict__ 277 | 278 | _write_mech_events_data_to_file(mech_events_data=mech_events_data) 279 | 280 | _write_mech_events_data_to_file( 281 | mech_events_data=mech_events_data, force_write=True 282 | ) 283 | 284 | except KeyboardInterrupt: 285 | print( 286 | "\n" 287 | "WARNING: The update of the local Mech events database was cancelled. " 288 | "Therefore, the Mech calls and costs might not be reflected accurately. " 289 | "You may attempt to rerun this script to retry synchronizing the database." 290 | ) 291 | input("Press Enter to continue...") 292 | except Exception as e: # pylint: disable=broad-except 293 | print(e) 294 | print( 295 | "WARNING: An error occurred while updating the local Mech events database. " 296 | "Therefore, the Mech calls and costs might not be reflected accurately. " 297 | "You may attempt to rerun this script to retry synchronizing the database." 298 | ) 299 | input("Press Enter to continue...") 300 | 301 | print("") 302 | 303 | 304 | def _get_mech_events(sender: str, event_cls: type[MechBaseEvent]) -> Dict[str, Any]: 305 | """Updates the local database of Mech events and returns the Mech events.""" 306 | 307 | _update_mech_events_db(sender, event_cls) 308 | mech_events_data = _read_mech_events_data_from_file() 309 | sender_data = mech_events_data.get(sender, {}) 310 | return sender_data.get(event_cls.event_name, {}) 311 | 312 | 313 | def get_mech_requests( 314 | sender: str, 315 | from_timestamp: float = DEFAULT_FROM_TIMESTAMP, 316 | to_timestamp: float = DEFAULT_TO_TIMESTAMP, 317 | ) -> Dict[str, Any]: 318 | """Returns the Mech requests.""" 319 | 320 | all_mech_events = _get_mech_events(sender, MechRequest) 321 | filtered_mech_events = {} 322 | for event_id, event_data in all_mech_events.items(): 323 | block_timestamp = int(event_data["block_timestamp"]) 324 | if from_timestamp <= block_timestamp <= to_timestamp: 325 | filtered_mech_events[event_id] = event_data 326 | 327 | return filtered_mech_events 328 | -------------------------------------------------------------------------------- /scripts/service_hash.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # ------------------------------------------------------------------------------ 4 | # 5 | # Copyright 2022-2023 Valory AG 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | # ------------------------------------------------------------------------------ 20 | 21 | """This script gets the service hash of a specific service on the registry.""" 22 | 23 | import json 24 | from typing import List 25 | from pathlib import Path 26 | 27 | import requests 28 | from web3 import Web3, HTTPProvider 29 | 30 | SCRIPT_PATH = Path(__file__).resolve().parent 31 | STORE_PATH = Path(SCRIPT_PATH, "..", ".trader_runner") 32 | DOTENV_PATH = Path(STORE_PATH, ".env") 33 | RPC_PATH = Path(STORE_PATH, "rpc.txt") 34 | SERVICE_ID_PATH = Path(STORE_PATH, "service_id.txt") 35 | PACKAGES_PATH = Path(SCRIPT_PATH, "..", "trader", "packages") 36 | 37 | REGISTRY_JSON = PACKAGES_PATH / "valory" / "contracts" / "service_registry" / "build" / "ServiceRegistryL2.json" 38 | REGISTRY_ADDRESS = "0x9338b5153AE39BB89f50468E608eD9d764B755fD" 39 | AUTONOLAS_GATEWAY = "https://gateway.autonolas.tech/ipfs/" 40 | URI_HASH_POSITION = 7 41 | 42 | 43 | def _get_hash_from_ipfs(hash_decoded: str) -> str: 44 | """Get the service's `bafybei` hash from IPFS.""" 45 | res = requests.get(f"{AUTONOLAS_GATEWAY}{hash_decoded}") 46 | if res.status_code == 200: 47 | return res.json().get("code_uri", "")[URI_HASH_POSITION:] 48 | raise ValueError(f"Something went wrong while trying to get the code uri from IPFS: {res}") 49 | 50 | 51 | def get_hash() -> str: 52 | """Get the service's hash.""" 53 | contract_data = json.loads(registry_json) 54 | abi = contract_data.get('abi', []) 55 | 56 | w3 = Web3(HTTPProvider(rpc)) 57 | contract_instance = w3.eth.contract(address=REGISTRY_ADDRESS, abi=abi) 58 | hash_encoded = contract_instance.functions.getService(int(service_id)).call()[2] 59 | hash_decoded = f"f01701220{hash_encoded.hex()}" 60 | hash_ = _get_hash_from_ipfs(hash_decoded) 61 | 62 | return hash_ 63 | 64 | 65 | def _parse_args() -> List[str]: 66 | """Parse the RPC and service id.""" 67 | params = [] 68 | for path in (RPC_PATH, SERVICE_ID_PATH, REGISTRY_JSON): 69 | with open(path) as file: 70 | params.append(file.read()) 71 | return params 72 | 73 | 74 | if __name__ == "__main__": 75 | rpc, service_id, registry_json = _parse_args() 76 | print(get_hash()) 77 | -------------------------------------------------------------------------------- /scripts/staking.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # ------------------------------------------------------------------------------ 4 | # 5 | # Copyright 2023-2024 Valory AG 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | # ------------------------------------------------------------------------------ 20 | 21 | """This script performs staking related operations.""" 22 | 23 | import argparse 24 | import os 25 | import sys 26 | import time 27 | import traceback 28 | from datetime import datetime 29 | from dotenv import dotenv_values 30 | from pathlib import Path 31 | 32 | import dotenv 33 | from aea_ledger_ethereum.ethereum import EthereumApi, EthereumCrypto 34 | from choose_staking import ( 35 | STAKING_PROGRAMS, 36 | DEPRECATED_STAKING_PROGRAMS, 37 | NO_STAKING_PROGRAM_ID, 38 | ) 39 | from utils import ( 40 | get_available_rewards, 41 | get_available_staking_slots, 42 | get_liveness_period, 43 | get_min_staking_duration, 44 | get_next_checkpoint_ts, 45 | get_service_ids, 46 | get_service_info, 47 | get_stake_txs, 48 | get_unstake_txs, 49 | is_service_evicted, 50 | is_service_staked, 51 | send_tx_and_wait_for_receipt, 52 | ) 53 | 54 | SCRIPT_PATH = Path(__file__).resolve().parent 55 | STORE_PATH = Path(SCRIPT_PATH, "..", ".trader_runner") 56 | DOTENV_PATH = Path(STORE_PATH, ".env") 57 | 58 | 59 | def _format_duration(duration_seconds: int) -> str: 60 | days, remainder = divmod(duration_seconds, 86400) 61 | hours, remainder = divmod(remainder, 3600) 62 | minutes, _ = divmod(remainder, 60) 63 | formatted_duration = f"{days}D {hours}h {minutes}m" 64 | return formatted_duration 65 | 66 | 67 | def _check_unstaking_availability( 68 | ledger_api: EthereumApi, 69 | service_id: int, 70 | staking_contract_address: str, 71 | staking_program: str, 72 | ) -> bool: 73 | """Service can only be unstaked if one of these conditions occur: 74 | - No rewards available 75 | - Staked for longer than > minimum_staking_durtion. 76 | A service can NOT be unstaked if evicted but has been staked for < minimum_staking_duration 77 | """ 78 | 79 | now = time.time() 80 | ts_start = get_service_info( 81 | ledger_api, service_id, staking_contract_address 82 | )[3] 83 | minimum_staking_duration = get_min_staking_duration( 84 | ledger_api, staking_contract_address 85 | ) 86 | available_rewards = get_available_rewards(ledger_api, staking_contract_address) 87 | if (now - ts_start) < minimum_staking_duration and available_rewards > 0: 88 | print( 89 | f"WARNING: Your service has been staked on {staking_program} for {_format_duration(int(now - ts_start))}." 90 | ) 91 | print( 92 | f"You cannot unstake your service from {staking_program} until it has been staked for at least {_format_duration(minimum_staking_duration)}." 93 | ) 94 | return False 95 | 96 | return True 97 | 98 | 99 | def _get_current_staking_program(ledger_api, service_id): 100 | all_staking_programs = STAKING_PROGRAMS.copy() 101 | all_staking_programs.update(DEPRECATED_STAKING_PROGRAMS) 102 | del all_staking_programs[NO_STAKING_PROGRAM_ID] 103 | del all_staking_programs["quickstart_alpha_everest"] # Very old program, not used likely - causes issues on "is_service_staked" 104 | 105 | staking_program = NO_STAKING_PROGRAM_ID 106 | staking_contract_address = None 107 | for program, address in all_staking_programs.items(): 108 | if is_service_staked( 109 | ledger_api, service_id, address 110 | ): 111 | staking_program = program 112 | staking_contract_address = address 113 | print(f"Service {service_id} is staked on {program}.") 114 | else: 115 | print(f"Service {service_id} is not staked on {program}.") 116 | return staking_contract_address, staking_program 117 | 118 | 119 | def _try_unstake_service( 120 | ledger_api: EthereumApi, 121 | service_id: int, 122 | owner_crypto: EthereumCrypto, 123 | warn_if_checkpoint_unavailable: bool = True, 124 | ) -> None: 125 | 126 | staking_contract_address, staking_program = _get_current_staking_program(ledger_api, service_id) 127 | print("") 128 | 129 | # Exit if not staked 130 | if staking_contract_address is None: 131 | print(f"Service {service_id} is not staked in any active program.") 132 | return 133 | else: 134 | print(f"Service {service_id} is staked on {staking_program}.") 135 | 136 | env_file_vars = dotenv_values(DOTENV_PATH) 137 | target_program = env_file_vars.get("STAKING_PROGRAM") 138 | print(f"Target program is set to {target_program}.") 139 | print("") 140 | 141 | # Collect information 142 | next_ts = get_next_checkpoint_ts(ledger_api, staking_contract_address) 143 | liveness_period = get_liveness_period(ledger_api, staking_contract_address) 144 | last_ts = next_ts - liveness_period 145 | now = time.time() 146 | 147 | if is_service_evicted( 148 | ledger_api, service_id, staking_contract_address 149 | ): 150 | print( 151 | f"WARNING: Service {service_id} has been evicted from the {staking_program} staking program due to inactivity." 152 | ) 153 | if os.environ.get("ATTENDED") == "true": 154 | input("Press Enter to continue...") 155 | 156 | can_unstake = _check_unstaking_availability( 157 | ledger_api, 158 | service_id, 159 | staking_contract_address, 160 | staking_program, 161 | ) 162 | 163 | if not can_unstake: 164 | print("Terminating script.") 165 | sys.exit(1) 166 | 167 | if warn_if_checkpoint_unavailable and (now < next_ts): 168 | formatted_last_ts = datetime.utcfromtimestamp(last_ts).strftime( 169 | "%Y-%m-%d %H:%M:%S UTC" 170 | ) 171 | formatted_next_ts = datetime.utcfromtimestamp(next_ts).strftime( 172 | "%Y-%m-%d %H:%M:%S UTC" 173 | ) 174 | 175 | print( 176 | "WARNING: Staking checkpoint call not available yet\n" 177 | "--------------------------------------------------\n" 178 | f"The liveness period ({liveness_period/3600} hours) has not passed since the last checkpoint call.\n" 179 | f" - {formatted_last_ts} - Last checkpoint call.\n" 180 | f" - {formatted_next_ts} - Next checkpoint call availability.\n" 181 | "\n" 182 | "If you proceed with unstaking, your agent's work done between the last checkpoint call until now will not be accounted for rewards.\n" 183 | "(Note: To maximize agent work eligible for rewards, the recommended practice is to unstake shortly after a checkpoint has been called and stake again immediately after.)\n" 184 | ) 185 | 186 | user_input = "y" 187 | if os.environ.get("ATTENDED") == "true": 188 | user_input = input( 189 | f"Do you want to continue unstaking service {service_id} from {staking_program}? (yes/no)\n" 190 | ).lower() 191 | print() 192 | 193 | if user_input not in ["yes", "y"]: 194 | print("Terminating script.") 195 | sys.exit(1) 196 | 197 | print(f"Unstaking service {service_id} from {staking_program}...") 198 | unstake_txs = get_unstake_txs( 199 | ledger_api, service_id, staking_contract_address 200 | ) 201 | for tx in unstake_txs: 202 | send_tx_and_wait_for_receipt(ledger_api, owner_crypto, tx) 203 | print( 204 | f"Successfully unstaked service {service_id} from {staking_program}." 205 | ) 206 | 207 | 208 | def _try_stake_service( 209 | ledger_api: EthereumApi, 210 | service_id: int, 211 | owner_crypto: EthereumCrypto, 212 | service_registry_address: str, 213 | staking_contract_address: str, 214 | staking_program: str, 215 | ) -> None: 216 | 217 | print(f"Service {service_id} has set {staking_program} staking program.") 218 | 219 | if staking_program == "no_staking": 220 | return 221 | 222 | if get_available_staking_slots(ledger_api, staking_contract_address) > 0: 223 | print( 224 | f"Service {service_id} is not staked on {staking_program}. Checking for available rewards..." 225 | ) 226 | available_rewards = get_available_rewards(ledger_api, staking_contract_address) 227 | if available_rewards == 0: 228 | # no rewards available, do nothing 229 | print(f"No rewards available on the {staking_program} staking program. Service {service_id} cannot be staked.") 230 | print("Please choose another staking program.") 231 | print("Terminating script.") 232 | sys.exit(1) 233 | 234 | print( 235 | f"Rewards available on {staking_program}: {available_rewards/10**18:.2f} OLAS. Staking service {service_id}..." 236 | ) 237 | stake_txs = get_stake_txs( 238 | ledger_api, 239 | service_id, 240 | service_registry_address, 241 | staking_contract_address, 242 | ) 243 | for tx in stake_txs: 244 | send_tx_and_wait_for_receipt(ledger_api, owner_crypto, tx) 245 | 246 | print(f"Service {service_id} staked successfully on {staking_program}.") 247 | else: 248 | print( 249 | f"All staking slots for contract {staking_contract_address} are taken. Service {service_id} cannot be staked." 250 | ) 251 | print("The script will finish.") 252 | sys.exit(1) 253 | 254 | 255 | def main() -> None: 256 | try: 257 | 258 | parser = argparse.ArgumentParser( 259 | description="Stake or unstake the service based on the state." 260 | ) 261 | parser.add_argument( 262 | "service_id", 263 | type=int, 264 | help="The on-chain service id.", 265 | ) 266 | parser.add_argument( 267 | "service_registry_address", 268 | type=str, 269 | help="The service registry contract address.", 270 | ) 271 | parser.add_argument( 272 | "staking_contract_address", 273 | type=str, 274 | help="The staking contract address.", 275 | ) 276 | parser.add_argument( 277 | "owner_private_key_path", 278 | type=str, 279 | help="Path to the file containing the service owner's Ethereum private key", 280 | ) 281 | parser.add_argument("rpc", type=str, help="RPC for the Gnosis chain") 282 | parser.add_argument( 283 | "unstake", 284 | type=bool, 285 | help="True if the service should be unstaked, False if it should be staked", 286 | default=False, 287 | ) 288 | parser.add_argument("--password", type=str, help="Private key password") 289 | args = parser.parse_args() 290 | 291 | env_file_vars = dotenv_values(DOTENV_PATH) 292 | target_program = env_file_vars.get("STAKING_PROGRAM") 293 | 294 | print(f"Starting {Path(__file__).name} script ({target_program})...\n") 295 | 296 | ledger_api = EthereumApi(address=args.rpc) 297 | owner_crypto = EthereumCrypto( 298 | private_key_path=args.owner_private_key_path, password=args.password 299 | ) 300 | 301 | # -------------- 302 | # Unstaking flow 303 | # -------------- 304 | if args.unstake: 305 | _try_unstake_service( 306 | ledger_api=ledger_api, 307 | service_id=args.service_id, 308 | owner_crypto=owner_crypto, 309 | ) 310 | return 311 | 312 | # -------------- 313 | # Staking flow 314 | # -------------- 315 | current_staking_contract_address, current_program = _get_current_staking_program(ledger_api, args.service_id) 316 | is_staked = current_program != NO_STAKING_PROGRAM_ID 317 | 318 | if is_staked and current_program != target_program: 319 | print( 320 | f"WARNING: Service {args.service_id} is staked on {current_program}, but target program is {target_program}. Unstaking..." 321 | ) 322 | _try_unstake_service( 323 | ledger_api=ledger_api, 324 | service_id=args.service_id, 325 | owner_crypto=owner_crypto, 326 | ) 327 | is_staked = False 328 | 329 | if is_staked and is_service_evicted( 330 | ledger_api, args.service_id, current_staking_contract_address 331 | ): 332 | print( 333 | f"Service {args.service_id} has been evicted from the {current_program} staking program due to inactivity. Unstaking..." 334 | ) 335 | _try_unstake_service( 336 | ledger_api=ledger_api, 337 | service_id=args.service_id, 338 | owner_crypto=owner_crypto, 339 | warn_if_checkpoint_unavailable=False, 340 | ) 341 | is_staked = False 342 | 343 | if is_staked and get_available_rewards(ledger_api, current_staking_contract_address) == 0: 344 | print( 345 | f"No rewards available on the {current_program} staking program. Unstaking service {args.service_id} from {current_program}..." 346 | ) 347 | _try_unstake_service( 348 | ledger_api=ledger_api, 349 | service_id=args.service_id, 350 | owner_crypto=owner_crypto, 351 | ) 352 | is_staked = False 353 | elif is_staked: 354 | print( 355 | f"There are rewards available. The service {args.service_id} should remain staked." 356 | ) 357 | 358 | if is_staked: 359 | print( 360 | f"Service {args.service_id} is already staked on {target_program}. " 361 | f"Checking if the staking contract has any rewards..." 362 | ) 363 | else: 364 | 365 | # At this point must be ensured all these conditions 366 | # 367 | # USE_STAKING==True 368 | # staking_state==Unstaked 369 | # available_slots > 0 370 | # available_rewards > 0 371 | # staking params==OK 372 | # state==DEPLOYED 373 | 374 | _try_stake_service( 375 | ledger_api=ledger_api, 376 | service_id=args.service_id, 377 | owner_crypto=owner_crypto, 378 | service_registry_address=args.service_registry_address, 379 | staking_contract_address=args.staking_contract_address, 380 | staking_program=target_program, 381 | ) 382 | 383 | except Exception as e: # pylint: disable=broad-except 384 | print(f"An error occurred while executing {Path(__file__).name}: {str(e)}") 385 | traceback.print_exc() 386 | sys.exit(1) 387 | 388 | 389 | if __name__ == "__main__": 390 | main() 391 | -------------------------------------------------------------------------------- /scripts/swap_safe_owner.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # ------------------------------------------------------------------------------ 4 | # 5 | # Copyright 2022-2023 Valory AG 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | # ------------------------------------------------------------------------------ 20 | 21 | """This script swaps ownership of a Safe with a single owner.""" 22 | 23 | import argparse 24 | import binascii 25 | import sys 26 | import traceback 27 | import typing 28 | from pathlib import Path 29 | 30 | from aea.contracts.base import Contract 31 | from aea_ledger_ethereum.ethereum import EthereumApi, EthereumCrypto 32 | from hexbytes import HexBytes 33 | from web3 import HTTPProvider, Web3 34 | 35 | from packages.valory.contracts.gnosis_safe.contract import ( 36 | GnosisSafeContract, 37 | SafeOperation, 38 | ) 39 | from packages.valory.contracts.multisend.contract import ( 40 | MultiSendContract, 41 | MultiSendOperation, 42 | ) 43 | from packages.valory.skills.transaction_settlement_abci.payload_tools import ( 44 | hash_payload_to_hex, 45 | skill_input_hex_to_payload, 46 | ) 47 | 48 | 49 | ContractType = typing.TypeVar("ContractType") 50 | 51 | 52 | def load_contract(ctype: ContractType) -> ContractType: 53 | """Load contract.""" 54 | *parts, _ = ctype.__module__.split(".") 55 | path = "/".join(parts) 56 | return Contract.from_dir(directory=path) 57 | 58 | 59 | if __name__ == "__main__": 60 | try: 61 | print(f" - Starting {Path(__file__).name} script...") 62 | 63 | parser = argparse.ArgumentParser( 64 | description="Swap ownership of a Safe with a single owner on the Gnosis chain." 65 | ) 66 | parser.add_argument( 67 | "safe_address", 68 | type=str, 69 | help="Safe address", 70 | ) 71 | parser.add_argument( 72 | "current_owner_private_key_path", 73 | type=str, 74 | help="Path to the file containing the Ethereum private key", 75 | ) 76 | parser.add_argument( 77 | "new_owner_address", type=str, help="Recipient address on the Gnosis chain" 78 | ) 79 | parser.add_argument("rpc", type=str, help="RPC for the Gnosis chain") 80 | parser.add_argument("--password", type=str, help="Private key password") 81 | args = parser.parse_args() 82 | 83 | ledger_api = EthereumApi(address=args.rpc) 84 | current_owner_crypto: EthereumCrypto 85 | current_owner_crypto = EthereumCrypto( 86 | private_key_path=args.current_owner_private_key_path, password=args.password 87 | ) 88 | owner_cryptos = [current_owner_crypto] # type: ignore 89 | 90 | owners = [ 91 | ledger_api.api.to_checksum_address(owner_crypto.address) 92 | for owner_crypto in owner_cryptos 93 | ] 94 | 95 | owner_to_swap = owners[0] 96 | 97 | print(f" - Safe address: {args.safe_address}") 98 | print(f" - Current owner: {owner_to_swap}") 99 | print(f" - New owner: {args.new_owner_address}") 100 | 101 | multisig_address = args.safe_address 102 | multisend_address = "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D" 103 | 104 | print(" - Loading contracts...") 105 | 106 | safe = load_contract(GnosisSafeContract) 107 | multisend = load_contract(MultiSendContract) 108 | 109 | print(" - Building Safe.swapOwner transaction...") 110 | 111 | multisend_txs = [] 112 | 113 | txd = safe.get_swap_owner_data( 114 | ledger_api=ledger_api, 115 | contract_address=multisig_address, 116 | old_owner=ledger_api.api.to_checksum_address(owner_to_swap), 117 | new_owner=ledger_api.api.to_checksum_address(args.new_owner_address), 118 | ).get("data") 119 | multisend_txs.append( 120 | { 121 | "operation": MultiSendOperation.CALL, 122 | "to": multisig_address, 123 | "value": 0, 124 | "data": HexBytes(txd[2:]), 125 | } 126 | ) 127 | 128 | multisend_txd = multisend.get_tx_data( # type: ignore 129 | ledger_api=ledger_api, 130 | contract_address=multisend_address, 131 | multi_send_txs=multisend_txs, 132 | ).get("data") 133 | multisend_data = bytes.fromhex(multisend_txd[2:]) 134 | 135 | safe_tx_hash = safe.get_raw_safe_transaction_hash( 136 | ledger_api=ledger_api, 137 | contract_address=multisig_address, 138 | to_address=multisend_address, 139 | value=0, 140 | data=multisend_data, 141 | safe_tx_gas=0, 142 | operation=SafeOperation.DELEGATE_CALL.value, 143 | ).get("tx_hash")[2:] 144 | 145 | payload_data = hash_payload_to_hex( 146 | safe_tx_hash=safe_tx_hash, 147 | ether_value=0, 148 | safe_tx_gas=0, 149 | to_address=multisend_address, 150 | data=multisend_data, 151 | ) 152 | 153 | tx_params = skill_input_hex_to_payload(payload=payload_data) 154 | safe_tx_bytes = binascii.unhexlify(tx_params["safe_tx_hash"]) 155 | owner_to_signature = {} 156 | 157 | print(" - Signing Safe.swapOwner transaction...") 158 | 159 | for owner_crypto in owner_cryptos: 160 | signature = owner_crypto.sign_message( 161 | message=safe_tx_bytes, 162 | is_deprecated_mode=True, 163 | ) 164 | owner_to_signature[ 165 | ledger_api.api.to_checksum_address(owner_crypto.address) 166 | ] = signature[2:] 167 | 168 | tx = safe.get_raw_safe_transaction( 169 | ledger_api=ledger_api, 170 | contract_address=multisig_address, 171 | sender_address=current_owner_crypto.address, 172 | owners=tuple(owners), # type: ignore 173 | to_address=tx_params["to_address"], 174 | value=tx_params["ether_value"], 175 | data=tx_params["data"], 176 | safe_tx_gas=tx_params["safe_tx_gas"], 177 | signatures_by_owner=owner_to_signature, 178 | operation=SafeOperation.DELEGATE_CALL.value, 179 | ) 180 | stx = current_owner_crypto.sign_transaction(tx) 181 | tx_digest = ledger_api.send_signed_transaction(stx) 182 | 183 | w3 = Web3(HTTPProvider(args.rpc)) 184 | 185 | print(f" - Safe.swapOwner transaction sent. Transaction hash: {tx_digest}") 186 | print(" - Waiting for transaction receipt...") 187 | receipt = w3.eth.wait_for_transaction_receipt(tx_digest) 188 | 189 | if receipt["status"] == 1: 190 | print(" - Safe.swapOwner transaction successfully mined.") 191 | print( 192 | f" - Safe owner successfully swapped from {owner_to_swap} to {args.new_owner_address}" 193 | ) 194 | else: 195 | print(" - Safe.swapOwner transaction failed to be mined.") 196 | sys.exit(1) 197 | 198 | except Exception as e: # pylint: disable=broad-except 199 | print(f"An error occurred while executing {Path(__file__).name}: {str(e)}") 200 | traceback.print_exc() 201 | sys.exit(1) 202 | -------------------------------------------------------------------------------- /scripts/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # ------------------------------------------------------------------------------ 3 | # 4 | # Copyright 2023 Valory AG 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # ------------------------------------------------------------------------------ 19 | 20 | """This package contains utils for working with the staking contract.""" 21 | 22 | import typing 23 | from datetime import datetime 24 | import time 25 | 26 | from aea.contracts.base import Contract 27 | from aea_ledger_ethereum.ethereum import EthereumApi, EthereumCrypto 28 | from eth_typing import HexStr 29 | 30 | from packages.valory.contracts.erc20.contract import ( 31 | ERC20, 32 | ) 33 | from packages.valory.contracts.service_staking_token.contract import ( 34 | ServiceStakingTokenContract, 35 | ) 36 | from packages.valory.contracts.staking_token.contract import StakingTokenContract 37 | from autonomy.chain.tx import ( 38 | TxSettler, 39 | should_retry, 40 | should_reprice, 41 | ) 42 | from requests.exceptions import ConnectionError as RequestsConnectionError 43 | from autonomy.chain.exceptions import ( 44 | ChainInteractionError, 45 | ChainTimeoutError, 46 | RPCError, 47 | TxBuildError, 48 | ) 49 | from autonomy.chain.config import ChainType 50 | from dotenv import dotenv_values 51 | from choose_staking import ZERO_ADDRESS 52 | from packages.valory.skills.staking_abci.rounds import StakingState 53 | from pathlib import Path 54 | 55 | SCRIPT_PATH = Path(__file__).resolve().parent 56 | STORE_PATH = Path(SCRIPT_PATH, "..", ".trader_runner") 57 | DOTENV_PATH = Path(STORE_PATH, ".env") 58 | 59 | DEFAULT_ON_CHAIN_INTERACT_TIMEOUT = 120.0 60 | DEFAULT_ON_CHAIN_INTERACT_RETRIES = 10 61 | DEFAULT_ON_CHAIN_INTERACT_SLEEP = 6.0 62 | 63 | 64 | ZERO_ETH = 0 65 | 66 | ContractType = typing.TypeVar("ContractType") 67 | 68 | GAS_PARAMS = { 69 | "maxFeePerGas": 30_000_000_000, 70 | "maxPriorityFeePerGas": 3_000_000_000, 71 | "gas": 500_000, 72 | } 73 | 74 | def load_contract(ctype: ContractType) -> ContractType: 75 | """Load contract.""" 76 | *parts, _ = ctype.__module__.split(".") 77 | path = "/".join(parts) 78 | return Contract.from_dir(directory=path) 79 | 80 | 81 | def get_approval_tx( 82 | ledger_api: EthereumApi, 83 | token: str, 84 | spender: str, 85 | amount: int, 86 | ) -> typing.Dict[str, typing.Any]: 87 | """Get approval tx""" 88 | approval_tx_data = erc20.build_approval_tx( 89 | ledger_api, 90 | token, 91 | spender, 92 | amount, 93 | ).pop("data") 94 | approval_tx = { 95 | "data": approval_tx_data, 96 | "to": token, 97 | "value": ZERO_ETH, 98 | } 99 | return approval_tx 100 | 101 | 102 | def get_balances( 103 | ledger_api: EthereumApi, 104 | token: str, 105 | owner: str, 106 | ) -> typing.Tuple[int, int]: 107 | """Returns the native and token balance of owner.""" 108 | balances = erc20.check_balance(ledger_api, token, owner) 109 | token_balance, native_balance = balances.pop("token"), balances.pop("wallet") 110 | return token_balance, native_balance 111 | 112 | 113 | def get_allowance( 114 | ledger_api: EthereumApi, 115 | token: str, 116 | owner: str, 117 | spender: str, 118 | ) -> int: 119 | """Returns the allowance of owner for spender.""" 120 | allowance = erc20.get_allowance(ledger_api, token, owner, spender).pop("data") 121 | return allowance 122 | 123 | 124 | def get_stake_txs( 125 | ledger_api: EthereumApi, 126 | service_id: int, 127 | service_registry_address: str, 128 | staking_contract_address: str, 129 | ) -> typing.List: 130 | """Stake the service""" 131 | # 1. approve the service to make use of the 132 | 133 | # we make use of the ERC20 contract to build the approval transaction 134 | # since it has the same interface as ERC721 135 | # we use the ZERO_ADDRESS as the contract address since we don't do any contract interaction here, 136 | # we are simply encoding 137 | approval_tx = get_approval_tx( 138 | ledger_api, service_registry_address, staking_contract_address, service_id 139 | ) 140 | 141 | # 2. stake the service 142 | stake_tx_data = staking_contract.build_stake_tx( 143 | ledger_api, staking_contract_address, service_id 144 | ).pop("data") 145 | stake_tx = { 146 | "data": stake_tx_data, 147 | "to": staking_contract_address, 148 | "value": ZERO_ETH, 149 | } 150 | return [approval_tx, stake_tx] 151 | 152 | 153 | def get_unstake_txs( 154 | ledger_api: EthereumApi, service_id: int, staking_contract_address: str 155 | ) -> typing.List: 156 | """Get unstake txs""" 157 | 158 | unstake_tx_data = staking_contract.build_unstake_tx( 159 | ledger_api, staking_contract_address, service_id 160 | ).pop("data") 161 | unstake_tx = { 162 | "data": unstake_tx_data, 163 | "to": staking_contract_address, 164 | "value": ZERO_ETH, 165 | } 166 | 167 | return [unstake_tx] 168 | 169 | 170 | def get_available_rewards( 171 | ledger_api: EthereumApi, staking_contract_address: str 172 | ) -> int: 173 | """Get available rewards.""" 174 | rewards = staking_contract.available_rewards( 175 | ledger_api, staking_contract_address 176 | ).pop("data") 177 | return rewards 178 | 179 | 180 | def is_service_staked( 181 | ledger_api: EthereumApi, service_id: int, staking_contract_address: str 182 | ) -> bool: 183 | """Check if service is staked.""" 184 | # TODO Not best approach. This is required because staking.py might call 185 | # different contract versions. 186 | for staking_contract in all_staking_contracts: 187 | try: 188 | service_staking_state = staking_contract.get_service_staking_state( 189 | ledger_api, staking_contract_address, service_id 190 | ).pop("data") 191 | 192 | if isinstance(service_staking_state, int): 193 | service_staking_state = StakingState(service_staking_state) 194 | 195 | is_staked = service_staking_state == StakingState.STAKED or service_staking_state == StakingState.EVICTED 196 | return is_staked 197 | except: # noqa 198 | continue 199 | 200 | raise Exception("Unable to retrieve staking state.") 201 | 202 | 203 | def is_service_evicted( 204 | ledger_api: EthereumApi, service_id: int, staking_contract_address: str 205 | ) -> bool: 206 | """Check if service is staked.""" 207 | # TODO Not best approach. This is required because staking.py might call 208 | # different contract versions. 209 | for staking_contract in all_staking_contracts: 210 | try: 211 | service_staking_state = staking_contract.get_service_staking_state( 212 | ledger_api, staking_contract_address, service_id 213 | ).pop("data") 214 | 215 | if isinstance(service_staking_state, int): 216 | service_staking_state = StakingState(service_staking_state) 217 | 218 | is_evicted = service_staking_state == StakingState.EVICTED 219 | return is_evicted 220 | except: # noqa 221 | continue 222 | 223 | raise Exception("Unable to retrieve eviction state.") 224 | 225 | 226 | def get_next_checkpoint_ts( 227 | ledger_api: EthereumApi, staking_contract_address: str 228 | ) -> int: 229 | """Check if service is staked.""" 230 | checkpoint_ts = staking_contract.get_next_checkpoint_ts( 231 | ledger_api, staking_contract_address 232 | ).pop("data") 233 | return checkpoint_ts 234 | 235 | 236 | def get_staking_rewards( 237 | ledger_api: EthereumApi, service_id: int, staking_contract_address: str 238 | ) -> int: 239 | """Check if service is staked.""" 240 | rewards = staking_contract.get_staking_rewards( 241 | ledger_api, staking_contract_address, service_id 242 | ).pop("data") 243 | return rewards 244 | 245 | 246 | def get_liveness_period( 247 | ledger_api: EthereumApi, staking_contract_address: str 248 | ) -> int: 249 | """Get the liveness period.""" 250 | liveness_period = staking_contract.get_liveness_period( 251 | ledger_api, staking_contract_address 252 | ).pop("data") 253 | return liveness_period 254 | 255 | 256 | def get_min_staking_duration( 257 | ledger_api: EthereumApi, staking_contract_address: str 258 | ) -> int: 259 | """Get the liveness period.""" 260 | min_staking_duration = staking_contract.get_min_staking_duration( 261 | ledger_api, staking_contract_address 262 | ).pop("data") 263 | return min_staking_duration 264 | 265 | 266 | def get_service_info( 267 | ledger_api: EthereumApi, service_id: int, staking_contract_address: str 268 | ) -> typing.List: 269 | """Get the service info.""" 270 | info = staking_contract.get_service_info( 271 | ledger_api, staking_contract_address, service_id 272 | ).pop("data") 273 | return info 274 | 275 | 276 | def get_price_with_retries( 277 | ledger_api: EthereumApi, staking_contract_address: str, retries: int = 5 278 | ) -> int: 279 | """Get the price with retries.""" 280 | for i in range(retries): 281 | try: 282 | price = staking_contract.try_get_gas_pricing(ledger_api, staking_contract_address) 283 | return price 284 | except Exception as e: 285 | print(e) 286 | continue 287 | raise ValueError("Failed to get price after retries") 288 | 289 | 290 | def get_available_staking_slots( 291 | ledger_api: EthereumApi, staking_contract_address: str 292 | ) -> int: 293 | """Get available staking slots""" 294 | max_num_services = staking_contract.max_num_services( 295 | ledger_api, staking_contract_address).pop("data") 296 | 297 | service_ids = staking_contract.get_service_ids( 298 | ledger_api, staking_contract_address).pop("data") 299 | 300 | return max_num_services - len(service_ids) 301 | 302 | 303 | def get_service_ids( 304 | ledger_api: EthereumApi, staking_contract_address: str 305 | ) -> typing.List[int]: 306 | """Get service Ids""" 307 | service_ids = staking_contract.get_service_ids( 308 | ledger_api, staking_contract_address).pop("data") 309 | 310 | return service_ids 311 | 312 | 313 | def send_tx( 314 | ledger_api: EthereumApi, 315 | crypto: EthereumCrypto, 316 | raw_tx: typing.Dict[str, typing.Any], 317 | timeout: float = DEFAULT_ON_CHAIN_INTERACT_TIMEOUT, 318 | max_retries: int = DEFAULT_ON_CHAIN_INTERACT_RETRIES, 319 | sleep: float = DEFAULT_ON_CHAIN_INTERACT_SLEEP, 320 | ) -> str: 321 | """Send transaction.""" 322 | tx_dict = { 323 | **raw_tx, 324 | **GAS_PARAMS, 325 | "from": crypto.address, 326 | "nonce": ledger_api.api.eth.get_transaction_count(crypto.address), 327 | "chainId": ledger_api.api.eth.chain_id, 328 | } 329 | gas_params = ledger_api.try_get_gas_pricing() 330 | if gas_params is not None: 331 | tx_dict.update(gas_params) 332 | 333 | tx_settler = TxSettler(ledger_api, crypto, ChainType.CUSTOM) 334 | retries = 0 335 | tx_digest = None 336 | already_known = False 337 | deadline = datetime.now().timestamp() + timeout 338 | while retries < max_retries and deadline >= datetime.now().timestamp(): 339 | retries += 1 340 | try: 341 | if not already_known: 342 | tx_signed = crypto.sign_transaction(transaction=tx_dict) 343 | tx_digest = ledger_api.send_signed_transaction( 344 | tx_signed=tx_signed, 345 | raise_on_try=True, 346 | ) 347 | tx_receipt = ledger_api.api.eth.get_transaction_receipt( 348 | typing.cast(str, tx_digest) 349 | ) 350 | if tx_receipt is not None: 351 | return tx_receipt 352 | except RequestsConnectionError as e: 353 | raise RPCError("Cannot connect to the given RPC") from e 354 | except Exception as e: # pylint: disable=broad-except 355 | error = str(e) 356 | if tx_settler._already_known(error): 357 | already_known = True 358 | continue # pragma: nocover 359 | if not should_retry(error): 360 | raise ChainInteractionError(error) from e 361 | if should_reprice(error): 362 | print("Repricing the transaction...") 363 | tx_dict = tx_settler._reprice(typing.cast(typing.Dict, tx_dict)) 364 | continue 365 | print(f"Error occurred when interacting with chain: {e}; ") 366 | print(f"will retry in {sleep}...") 367 | time.sleep(sleep) 368 | raise ChainTimeoutError("Timed out when waiting for transaction to go through") 369 | 370 | 371 | def send_tx_and_wait_for_receipt( 372 | ledger_api: EthereumApi, 373 | crypto: EthereumCrypto, 374 | raw_tx: typing.Dict[str, typing.Any], 375 | ) -> typing.Dict[str, typing.Any]: 376 | """Send transaction and wait for receipt.""" 377 | receipt = HexStr(send_tx(ledger_api, crypto, raw_tx)) 378 | if receipt["status"] != 1: 379 | raise ValueError("Transaction failed. Receipt:", receipt) 380 | return receipt 381 | 382 | 383 | # TODO 'staking_contract' refers to the current active program.abs 384 | # There are methods above that will be called for other programs, 385 | # and whose ABI might differ. A "patch" is currently implemented, 386 | # but it should be refactored in a more elegant and robust way. 387 | env_file_vars = dotenv_values(DOTENV_PATH) 388 | activity_checker = env_file_vars.get("MECH_ACTIVITY_CHECKER_CONTRACT") 389 | 390 | if activity_checker is None or activity_checker == ZERO_ADDRESS: 391 | staking_contract = typing.cast( 392 | typing.Type[Contract], load_contract(ServiceStakingTokenContract) 393 | ) 394 | else: 395 | staking_contract = typing.cast( 396 | typing.Type[Contract], load_contract(StakingTokenContract) 397 | ) 398 | 399 | all_staking_contracts = [ 400 | typing.cast( 401 | typing.Type[Contract], load_contract(ServiceStakingTokenContract) 402 | ), 403 | typing.cast( 404 | typing.Type[Contract], load_contract(StakingTokenContract) 405 | ) 406 | ] 407 | 408 | erc20 = typing.cast(typing.Type[ERC20], load_contract(ERC20)) 409 | -------------------------------------------------------------------------------- /stop_service.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # ------------------------------------------------------------------------------ 4 | # 5 | # Copyright 2023 Valory AG 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | # ------------------------------------------------------------------------------ 20 | 21 | # force utf mode for python, cause sometimes there are issues with local codepages 22 | export PYTHONUTF8=1 23 | 24 | 25 | cd trader 26 | service_dir="trader_service" 27 | build_dir=$(ls -d "$service_dir"/abci_build_???? 2>/dev/null || echo "$service_dir/abci_build") 28 | poetry run autonomy deploy stop --build-dir "$build_dir" 29 | cd .. 30 | -------------------------------------------------------------------------------- /terminate_on_chain_service.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # ------------------------------------------------------------------------------ 4 | # 5 | # Copyright 2023-2024 Valory AG 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | # ------------------------------------------------------------------------------ 20 | 21 | 22 | # force utf mode for python, cause sometimes there are issues with local codepages 23 | export PYTHONUTF8=1 24 | 25 | 26 | # Get the address from a keys.json file 27 | get_address() { 28 | local keys_json_path="$1" 29 | 30 | if [ ! -f "$keys_json_path" ]; then 31 | echo "Error: $keys_json_path does not exist." 32 | return 1 33 | fi 34 | 35 | local address_start_position=17 36 | local address=$(sed -n 3p "$keys_json_path") 37 | address=$(echo "$address" | 38 | awk '{ print substr( $0, '$address_start_position', length($0) - '$address_start_position' - 1 ) }') 39 | 40 | echo -n "$address" 41 | } 42 | 43 | # Get the private key from a keys.json file 44 | get_private_key() { 45 | local keys_json_path="$1" 46 | 47 | if [ ! -f "$keys_json_path" ]; then 48 | echo "Error: $keys_json_path does not exist." 49 | return 1 50 | fi 51 | 52 | local private_key_start_position=21 53 | local private_key=$(sed -n 4p "$keys_json_path") 54 | private_key=$(echo -n "$private_key" | 55 | awk '{ printf substr( $0, '$private_key_start_position', length($0) - '$private_key_start_position' ) }') 56 | 57 | private_key=$(echo -n "$private_key" | awk '{gsub(/\\"/, "\"", $0); print $0}') 58 | private_key="${private_key#0x}" 59 | 60 | echo -n "$private_key" 61 | } 62 | 63 | # Function to retrieve on-chain service state (requires env variables set to use --use-custom-chain) 64 | get_on_chain_service_state() { 65 | local service_id="$1" 66 | local service_info=$(poetry run autonomy service --retries $RPC_RETRIES --timeout $RPC_TIMEOUT_SECONDS --use-custom-chain info "$service_id") 67 | local state="$(echo "$service_info" | awk '/Service State/ {sub(/\|[ \t]*Service State[ \t]*\|[ \t]*/, ""); sub(/[ \t]*\|[ \t]*/, ""); print}')" 68 | echo "$state" 69 | } 70 | 71 | # Asks password if key files are password-protected 72 | ask_password_if_needed() { 73 | agent_pkey=$(get_private_key "$keys_json_path") 74 | if [[ "$agent_pkey" = *crypto* ]]; then 75 | echo "Enter your password" 76 | echo "-------------------" 77 | echo "Your key files are protected with a password." 78 | read -s -p "Please, enter your password: " password 79 | use_password=true 80 | password_argument="--password $password" 81 | echo "" 82 | else 83 | echo "Your key files are not protected with a password." 84 | use_password=false 85 | password_argument="" 86 | fi 87 | echo "" 88 | } 89 | 90 | # Validates the provided password 91 | validate_password() { 92 | local is_password_valid_1=$(poetry run python ../scripts/is_keys_json_password_valid.py ../$keys_json_path $password_argument) 93 | local is_password_valid_2=$(poetry run python ../scripts/is_keys_json_password_valid.py ../$operator_keys_file $password_argument) 94 | 95 | if [ "$is_password_valid_1" != "True" ] || [ "$is_password_valid_2" != "True" ]; then 96 | echo "Could not decrypt key files. Please verify if your key files are password-protected, and if the provided password is correct (passwords are case-sensitive)." 97 | echo "Terminating the script." 98 | exit 1 99 | fi 100 | } 101 | 102 | export_dotenv() { 103 | local dotenv_path="$1" 104 | unamestr=$(uname) 105 | if [ "$unamestr" = 'Linux' ]; then 106 | export $(grep -v '^#' $dotenv_path | xargs -d '\n') 107 | elif [ "$unamestr" = 'FreeBSD' ] || [ "$unamestr" = 'Darwin' ]; then 108 | export $(grep -v '^#' $dotenv_path | xargs -0) 109 | fi 110 | } 111 | 112 | store=".trader_runner" 113 | env_file_path="$store/.env" 114 | rpc_path="$store/rpc.txt" 115 | operator_keys_file="$store/operator_keys.json" 116 | operator_pkey_path="$store/operator_pkey.txt" 117 | keys_json="keys.json" 118 | keys_json_path="$store/$keys_json" 119 | agent_pkey_path="$store/agent_pkey.txt" 120 | agent_address_path="$store/agent_address.txt" 121 | service_id_path="$store/service_id.txt" 122 | service_safe_address_path="$store/service_safe_address.txt" 123 | store_readme_path="$store/README.txt" 124 | 125 | source "$env_file_path" 126 | rpc=$(cat $rpc_path) 127 | operator_address=$(get_address $operator_keys_file) 128 | service_id=$(cat $service_id_path) 129 | unstake=true 130 | gnosis_chain_id=100 131 | 132 | # Define constants for on-chain interaction 133 | export RPC_RETRIES=40 134 | export RPC_TIMEOUT_SECONDS=120 135 | export CUSTOM_CHAIN_RPC=$rpc 136 | export CUSTOM_CHAIN_ID=$gnosis_chain_id 137 | export CUSTOM_SERVICE_MANAGER_ADDRESS="0x04b0007b2aFb398015B76e5f22993a1fddF83644" 138 | export CUSTOM_GNOSIS_SAFE_PROXY_FACTORY_ADDRESS="0x3C1fF68f5aa342D296d4DEe4Bb1cACCA912D95fE" 139 | export CUSTOM_GNOSIS_SAFE_SAME_ADDRESS_MULTISIG_ADDRESS="0x6e7f594f680f7aBad18b7a63de50F0FeE47dfD06" 140 | export CUSTOM_MULTISEND_ADDRESS="0x40A2aCCbd92BCA938b02010E17A5b8929b49130D" 141 | export WXDAI_ADDRESS="0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d" 142 | 143 | # Check if --attended flag is passed 144 | export ATTENDED=true 145 | for arg in "$@"; do 146 | if [ "$arg" = "--attended=false" ]; then 147 | export ATTENDED=false 148 | fi 149 | done 150 | 151 | export_dotenv "$env_file_path" 152 | 153 | set -e # Exit script on first error 154 | echo "--------------------------" 155 | echo "Terminate on-chain service" 156 | echo "--------------------------" 157 | echo "" 158 | echo "This script will terminate and unbond your on-chain service (id $service_id)." 159 | echo "If your service is staked, you will receive the staking funds to the owner/operator address:" 160 | echo "$operator_address" 161 | echo 162 | response="y" 163 | if [ "$attended" = true ]; then 164 | echo "Please, ensure that your service is stopped (./stop_service.sh) before proceeding." 165 | echo "Do you want to continue? (yes/no)" 166 | read -r response 167 | echo "" 168 | fi 169 | 170 | if [[ ! "$response" =~ ^([yY][eE][sS]|[yY])$ ]]; then 171 | echo "Cancelled." 172 | exit 0 173 | fi 174 | 175 | ask_password_if_needed 176 | cd trader 177 | validate_password 178 | 179 | # Unstake if applicable 180 | poetry run python "../scripts/staking.py" "$service_id" "$CUSTOM_SERVICE_REGISTRY_ADDRESS" "$CUSTOM_STAKING_ADDRESS" "../$operator_pkey_path" "$rpc" "$unstake" $password_argument; 181 | 182 | service_safe_address=$(cat "../$service_safe_address_path") 183 | current_safe_owners=$(poetry run python "../scripts/get_safe_owners.py" "$service_safe_address" "../$agent_pkey_path" "$rpc" $password_argument | awk '{gsub(/"/, "\047", $0); print $0}') 184 | agent_address=$(get_address "../$keys_json_path") 185 | 186 | # transfer the ownership of the Safe from the agent to the service owner 187 | # (in a live service, this should be done by sending a 0 DAI transfer to its Safe) 188 | if [[ "$current_safe_owners" == "['$agent_address']" ]]; then 189 | echo "[Agent instance] Swapping Safe owner..." 190 | poetry run python "../scripts/swap_safe_owner.py" "$service_safe_address" "../$agent_pkey_path" "$operator_address" "$rpc" $password_argument 191 | fi 192 | 193 | # terminate current service 194 | state="$(get_on_chain_service_state "$service_id")" 195 | if [ "$state" == "ACTIVE_REGISTRATION" ] || [ "$state" == "FINISHED_REGISTRATION" ] || [ "$state" == "DEPLOYED" ]; then 196 | echo "[Service owner] Terminating on-chain service $service_id..." 197 | poetry run autonomy service \ 198 | --retries $RPC_RETRIES \ 199 | --timeout $RPC_TIMEOUT_SECONDS \ 200 | --use-custom-chain \ 201 | terminate "$service_id" \ 202 | --key "../$operator_pkey_path" $password_argument 203 | fi 204 | 205 | # unbond current service 206 | if [ "$(get_on_chain_service_state "$service_id")" == "TERMINATED_BONDED" ]; then 207 | echo "[Operator] Unbonding on-chain service $service_id..." 208 | poetry run autonomy service \ 209 | --retries $RPC_RETRIES \ 210 | --timeout $RPC_TIMEOUT_SECONDS \ 211 | --use-custom-chain \ 212 | unbond "$service_id" \ 213 | --key "../$operator_pkey_path" $password_argument 214 | fi 215 | 216 | if [ "$(get_on_chain_service_state "$service_id")" == "PRE_REGISTRATION" ]; then 217 | echo "Service $service_id is now terminated and unbonded (i.e., it is on PRE-REGISTRATION state)." 218 | echo "You can check this on https://registry.olas.network/gnosis/services/$service_id." 219 | echo "In order to deploy your on-chain service again, please execute './run_service.sh'." 220 | fi 221 | echo "Finished." 222 | -------------------------------------------------------------------------------- /trades.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # ------------------------------------------------------------------------------ 4 | # 5 | # Copyright 2022-2024 Valory AG 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | # ------------------------------------------------------------------------------ 20 | 21 | """This script queries the OMEN subgraph to obtain the trades of a given address.""" 22 | 23 | import datetime 24 | import os 25 | import re 26 | from argparse import Action, ArgumentError, ArgumentParser, Namespace 27 | from collections import defaultdict 28 | from dotenv import load_dotenv 29 | from enum import Enum 30 | from pathlib import Path 31 | from string import Template 32 | from typing import Any, Dict, Optional 33 | 34 | import requests 35 | 36 | from scripts.mech_events import get_mech_requests 37 | 38 | 39 | IRRELEVANT_TOOLS = [ 40 | "openai-text-davinci-002", 41 | "openai-text-davinci-003", 42 | "openai-gpt-3.5-turbo", 43 | "openai-gpt-4", 44 | "stabilityai-stable-diffusion-v1-5", 45 | "stabilityai-stable-diffusion-xl-beta-v2-2-2", 46 | "stabilityai-stable-diffusion-512-v2-1", 47 | "stabilityai-stable-diffusion-768-v2-1", 48 | "deepmind-optimization-strong", 49 | "deepmind-optimization", 50 | ] 51 | QUERY_BATCH_SIZE = 1000 52 | DUST_THRESHOLD = 10000000000000 53 | INVALID_ANSWER = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF 54 | FPMM_CREATOR = "0x89c5cc945dd550bcffb72fe42bff002429f46fec" 55 | DEFAULT_FROM_DATE = "1970-01-01T00:00:00" 56 | DEFAULT_TO_DATE = "2038-01-19T03:14:07" 57 | DEFAULT_FROM_TIMESTAMP = 0 58 | DEFAULT_TO_TIMESTAMP = 2147483647 59 | SCRIPT_PATH = Path(__file__).resolve().parent 60 | STORE_PATH = Path(SCRIPT_PATH, ".trader_runner") 61 | RPC_PATH = Path(STORE_PATH, "rpc.txt") 62 | ENV_FILE = Path(STORE_PATH, ".env") 63 | WXDAI_CONTRACT_ADDRESS = "0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d" 64 | SCRIPT_PATH = Path(__file__).resolve().parent 65 | STORE_PATH = Path(SCRIPT_PATH, ".trader_runner") 66 | SAFE_ADDRESS_PATH = Path(STORE_PATH, "service_safe_address.txt") 67 | 68 | 69 | load_dotenv(ENV_FILE) 70 | 71 | 72 | headers = { 73 | "Accept": "application/json, multipart/mixed", 74 | "Content-Type": "application/json", 75 | } 76 | 77 | 78 | omen_xdai_trades_query = Template( 79 | """ 80 | { 81 | fpmmTrades( 82 | where: { 83 | type: Buy, 84 | creator: "${creator}", 85 | fpmm_: { 86 | creator: "${fpmm_creator}" 87 | creationTimestamp_gte: "${fpmm_creationTimestamp_gte}", 88 | creationTimestamp_lt: "${fpmm_creationTimestamp_lte}" 89 | }, 90 | creationTimestamp_gte: "${creationTimestamp_gte}", 91 | creationTimestamp_lte: "${creationTimestamp_lte}" 92 | creationTimestamp_gt: "${creationTimestamp_gt}" 93 | } 94 | first: ${first} 95 | orderBy: creationTimestamp 96 | orderDirection: asc 97 | ) { 98 | id 99 | title 100 | collateralToken 101 | outcomeTokenMarginalPrice 102 | oldOutcomeTokenMarginalPrice 103 | type 104 | creator { 105 | id 106 | } 107 | creationTimestamp 108 | collateralAmount 109 | collateralAmountUSD 110 | feeAmount 111 | outcomeIndex 112 | outcomeTokensTraded 113 | transactionHash 114 | fpmm { 115 | id 116 | outcomes 117 | title 118 | answerFinalizedTimestamp 119 | currentAnswer 120 | isPendingArbitration 121 | arbitrationOccurred 122 | openingTimestamp 123 | condition { 124 | id 125 | } 126 | } 127 | } 128 | } 129 | """ 130 | ) 131 | 132 | 133 | conditional_tokens_gc_user_query = Template( 134 | """ 135 | { 136 | user(id: "${id}") { 137 | userPositions( 138 | first: ${first} 139 | where: { 140 | id_gt: "${userPositions_id_gt}" 141 | } 142 | orderBy: id 143 | ) { 144 | balance 145 | id 146 | position { 147 | id 148 | conditionIds 149 | } 150 | totalBalance 151 | wrappedBalance 152 | } 153 | } 154 | } 155 | """ 156 | ) 157 | 158 | 159 | class MarketState(Enum): 160 | """Market state""" 161 | 162 | OPEN = 1 163 | PENDING = 2 164 | FINALIZING = 3 165 | ARBITRATING = 4 166 | CLOSED = 5 167 | UNKNOWN = 6 168 | 169 | def __str__(self) -> str: 170 | """Prints the market status.""" 171 | return self.name.capitalize() 172 | 173 | 174 | class MarketAttribute(Enum): 175 | """Attribute""" 176 | 177 | NUM_TRADES = "Num_trades" 178 | NUM_VALID_TRADES = "Num_valid_trades" 179 | WINNER_TRADES = "Winner_trades" 180 | NUM_REDEEMED = "Num_redeemed" 181 | NUM_INVALID_MARKET = "Num_invalid_market" 182 | INVESTMENT = "Investment" 183 | FEES = "Fees" 184 | MECH_CALLS = "Mech_calls" 185 | MECH_FEES = "Mech_fees" 186 | EARNINGS = "Earnings" 187 | NET_EARNINGS = "Net_earnings" 188 | REDEMPTIONS = "Redemptions" 189 | ROI = "ROI" 190 | 191 | def __str__(self) -> str: 192 | """Prints the attribute.""" 193 | return self.value 194 | 195 | def __repr__(self) -> str: 196 | """Prints the attribute representation.""" 197 | return self.name 198 | 199 | @staticmethod 200 | def argparse(s: str) -> "MarketAttribute": 201 | """Performs string conversion to MarketAttribute.""" 202 | try: 203 | return MarketAttribute[s.upper()] 204 | except KeyError as e: 205 | raise ValueError(f"Invalid MarketAttribute: {s}") from e 206 | 207 | 208 | STATS_TABLE_COLS = list(MarketState) + ["TOTAL"] 209 | STATS_TABLE_ROWS = list(MarketAttribute) 210 | 211 | 212 | def get_balance(address: str, rpc_url: str) -> int: 213 | """Get the native xDAI balance of an address in wei.""" 214 | headers = {"Content-Type": "application/json"} 215 | data = { 216 | "jsonrpc": "2.0", 217 | "method": "eth_getBalance", 218 | "params": [address, "latest"], 219 | "id": 1, 220 | } 221 | response = requests.post(rpc_url, headers=headers, json=data) 222 | return int(response.json().get("result"), 16) 223 | 224 | 225 | def get_token_balance( 226 | gnosis_address: str, token_contract_address: str, rpc_url: str 227 | ) -> int: 228 | """Get the token balance of an address in wei.""" 229 | function_selector = "70a08231" # function selector for balanceOf(address) 230 | padded_address = gnosis_address.replace("0x", "").rjust( 231 | 64, "0" 232 | ) # remove '0x' and pad the address to 32 bytes 233 | data = function_selector + padded_address 234 | 235 | payload = { 236 | "jsonrpc": "2.0", 237 | "method": "eth_call", 238 | "params": [{"to": token_contract_address, "data": data}, "latest"], 239 | "id": 1, 240 | } 241 | response = requests.post(rpc_url, json=payload) 242 | result = response.json().get("result", "0x0") 243 | balance_wei = int(result, 16) # convert hex to int 244 | return balance_wei 245 | 246 | 247 | class EthereumAddressAction(Action): 248 | """Argparse class to validate an Ethereum addresses.""" 249 | 250 | def __call__( 251 | self, 252 | parser: ArgumentParser, 253 | namespace: Namespace, 254 | values: Any, 255 | option_string: Optional[str] = None, 256 | ) -> None: 257 | """Validates an Ethereum addresses.""" 258 | 259 | address = values 260 | if not re.match(r"^0x[a-fA-F0-9]{40}$", address): 261 | raise ArgumentError(self, f"Invalid Ethereum address: {address}") 262 | setattr(namespace, self.dest, address) 263 | 264 | 265 | def _parse_args() -> Any: 266 | """Parse the script arguments.""" 267 | parser = ArgumentParser(description="Get trades on Omen for a Safe address.") 268 | parser.add_argument( 269 | "--creator", 270 | action=EthereumAddressAction, 271 | help="Ethereum address of the service Safe", 272 | ) 273 | parser.add_argument( 274 | "--from-date", 275 | type=datetime.datetime.fromisoformat, 276 | default=DEFAULT_FROM_DATE, 277 | help="Start date (UTC) in YYYY-MM-DD:HH:mm:ss format", 278 | ) 279 | parser.add_argument( 280 | "--to-date", 281 | type=datetime.datetime.fromisoformat, 282 | default=DEFAULT_TO_DATE, 283 | help="End date (UTC) in YYYY-MM-DD:HH:mm:ss format", 284 | ) 285 | parser.add_argument( 286 | "--fpmm-created-from-date", 287 | type=datetime.datetime.fromisoformat, 288 | default=DEFAULT_FROM_DATE, 289 | help="Start date (UTC) in YYYY-MM-DD:HH:mm:ss format", 290 | ) 291 | parser.add_argument( 292 | "--fpmm-created-to-date", 293 | type=datetime.datetime.fromisoformat, 294 | default=DEFAULT_TO_DATE, 295 | help="End date (UTC) in YYYY-MM-DD:HH:mm:ss format", 296 | ) 297 | args = parser.parse_args() 298 | 299 | if args.creator is None: 300 | with open(SAFE_ADDRESS_PATH, "r", encoding="utf-8") as file: 301 | args.creator = file.read().strip() 302 | 303 | args.from_date = args.from_date.replace(tzinfo=datetime.timezone.utc) 304 | args.to_date = args.to_date.replace(tzinfo=datetime.timezone.utc) 305 | args.fpmm_created_from_date = args.fpmm_created_from_date.replace( 306 | tzinfo=datetime.timezone.utc 307 | ) 308 | args.fpmm_created_to_date = args.fpmm_created_to_date.replace( 309 | tzinfo=datetime.timezone.utc 310 | ) 311 | 312 | return args 313 | 314 | 315 | def _to_content(q: str) -> Dict[str, Any]: 316 | """Convert the given query string to payload content, i.e., add it under a `queries` key and convert it to bytes.""" 317 | finalized_query = { 318 | "query": q, 319 | "variables": None, 320 | "extensions": {"headers": None}, 321 | } 322 | return finalized_query 323 | 324 | 325 | def _query_omen_xdai_subgraph( # pylint: disable=too-many-locals 326 | creator: str, 327 | from_timestamp: float = DEFAULT_FROM_TIMESTAMP, 328 | to_timestamp: float = DEFAULT_TO_TIMESTAMP, 329 | fpmm_from_timestamp: float = DEFAULT_FROM_TIMESTAMP, 330 | fpmm_to_timestamp: float = DEFAULT_TO_TIMESTAMP, 331 | ) -> Dict[str, Any]: 332 | """Query the subgraph.""" 333 | subgraph_api_key = os.getenv('SUBGRAPH_API_KEY') 334 | url = f"https://gateway-arbitrum.network.thegraph.com/api/{subgraph_api_key}/subgraphs/id/9fUVQpFwzpdWS9bq5WkAnmKbNNcoBwatMR4yZq81pbbz" 335 | 336 | grouped_results = defaultdict(list) 337 | creationTimestamp_gt = "0" 338 | 339 | while True: 340 | query = omen_xdai_trades_query.substitute( 341 | creator=creator.lower(), 342 | fpmm_creator=FPMM_CREATOR.lower(), 343 | creationTimestamp_gte=int(from_timestamp), 344 | creationTimestamp_lte=int(to_timestamp), 345 | fpmm_creationTimestamp_gte=int(fpmm_from_timestamp), 346 | fpmm_creationTimestamp_lte=int(fpmm_to_timestamp), 347 | first=QUERY_BATCH_SIZE, 348 | creationTimestamp_gt=creationTimestamp_gt, 349 | ) 350 | content_json = _to_content(query) 351 | res = requests.post(url, headers=headers, json=content_json) 352 | result_json = res.json() 353 | trades = result_json.get("data", {}).get("fpmmTrades", []) 354 | 355 | if not trades: 356 | break 357 | 358 | for trade in trades: 359 | fpmm_id = trade.get("fpmm", {}).get("id") 360 | grouped_results[fpmm_id].append(trade) 361 | 362 | creationTimestamp_gt = trades[len(trades) - 1]["creationTimestamp"] 363 | 364 | all_results = { 365 | "data": { 366 | "fpmmTrades": [ 367 | trade 368 | for trades_list in grouped_results.values() 369 | for trade in trades_list 370 | ] 371 | } 372 | } 373 | 374 | return all_results 375 | 376 | 377 | def _query_conditional_tokens_gc_subgraph(creator: str) -> Dict[str, Any]: 378 | """Query the subgraph.""" 379 | subgraph_api_key = os.getenv('SUBGRAPH_API_KEY') 380 | url = f"https://gateway-arbitrum.network.thegraph.com/api/{subgraph_api_key}/subgraphs/id/7s9rGBffUTL8kDZuxvvpuc46v44iuDarbrADBFw5uVp2" 381 | 382 | all_results: Dict[str, Any] = {"data": {"user": {"userPositions": []}}} 383 | userPositions_id_gt = "" 384 | while True: 385 | query = conditional_tokens_gc_user_query.substitute( 386 | id=creator.lower(), 387 | first=QUERY_BATCH_SIZE, 388 | userPositions_id_gt=userPositions_id_gt, 389 | ) 390 | content_json = {"query": query} 391 | res = requests.post(url, headers=headers, json=content_json) 392 | result_json = res.json() 393 | user_data = result_json.get("data", {}).get("user", {}) 394 | 395 | if not user_data: 396 | break 397 | 398 | user_positions = user_data.get("userPositions", []) 399 | 400 | if user_positions: 401 | all_results["data"]["user"]["userPositions"].extend(user_positions) 402 | userPositions_id_gt = user_positions[len(user_positions) - 1]["id"] 403 | else: 404 | break 405 | 406 | if len(all_results["data"]["user"]["userPositions"]) == 0: 407 | return {"data": {"user": None}} 408 | 409 | return all_results 410 | 411 | 412 | def wei_to_unit(wei: int) -> float: 413 | """Converts wei to currency unit.""" 414 | return wei / 10**18 415 | 416 | 417 | def wei_to_xdai(wei: int) -> str: 418 | """Converts and formats wei to xDAI.""" 419 | return "{:.2f} xDAI".format(wei_to_unit(wei)) 420 | 421 | 422 | def wei_to_wxdai(wei: int) -> str: 423 | """Converts and formats wei to WxDAI.""" 424 | return "{:.2f} WxDAI".format(wei_to_unit(wei)) 425 | 426 | 427 | def wei_to_olas(wei: int) -> str: 428 | """Converts and formats wei to WxDAI.""" 429 | return "{:.2f} OLAS".format(wei_to_unit(wei)) 430 | 431 | 432 | def _is_redeemed(user_json: Dict[str, Any], fpmmTrade: Dict[str, Any]) -> bool: 433 | user_positions = user_json["data"]["user"]["userPositions"] 434 | outcomes_tokens_traded = int(fpmmTrade["outcomeTokensTraded"]) 435 | condition_id = fpmmTrade["fpmm"]["condition"]["id"] 436 | 437 | for position in user_positions: 438 | position_condition_ids = position["position"]["conditionIds"] 439 | balance = int(position["balance"]) 440 | 441 | if condition_id in position_condition_ids and balance == outcomes_tokens_traded: 442 | return False 443 | 444 | for position in user_positions: 445 | position_condition_ids = position["position"]["conditionIds"] 446 | balance = int(position["balance"]) 447 | 448 | if condition_id in position_condition_ids and balance == 0: 449 | return True 450 | 451 | return False 452 | 453 | 454 | def _compute_roi(initial_value: int, final_value: int) -> float: 455 | if initial_value != 0: 456 | roi = (final_value - initial_value) / initial_value 457 | else: 458 | roi = 0.0 459 | 460 | return roi 461 | 462 | 463 | def _compute_totals( 464 | table: Dict[Any, Dict[Any, Any]], mech_statistics: Dict[str, Any] 465 | ) -> None: 466 | for row in table.keys(): 467 | total = sum(table[row][c] for c in table[row]) 468 | table[row]["TOTAL"] = total 469 | 470 | # Total mech fees and calls need to be recomputed, because there could be mech calls 471 | # for markets that were not traded 472 | total_mech_calls = 0 473 | total_mech_fees = 0 474 | 475 | for _, v in mech_statistics.items(): 476 | total_mech_calls += v["count"] 477 | total_mech_fees += v["fees"] 478 | 479 | table[MarketAttribute.MECH_CALLS]["TOTAL"] = total_mech_calls 480 | table[MarketAttribute.MECH_FEES]["TOTAL"] = total_mech_fees 481 | 482 | for col in STATS_TABLE_COLS: 483 | # Omen deducts the fee from collateral_amount (INVESTMENT) to compute outcomes_tokens_traded (EARNINGS). 484 | table[MarketAttribute.INVESTMENT][col] = ( 485 | table[MarketAttribute.INVESTMENT][col] - table[MarketAttribute.FEES][col] 486 | ) 487 | table[MarketAttribute.NET_EARNINGS][col] = ( 488 | table[MarketAttribute.EARNINGS][col] 489 | - table[MarketAttribute.INVESTMENT][col] 490 | - table[MarketAttribute.FEES][col] 491 | - table[MarketAttribute.MECH_FEES][col] 492 | ) 493 | # ROI is recomputed here for all columns, including TOTAL. 494 | table[MarketAttribute.ROI][col] = _compute_roi( 495 | table[MarketAttribute.INVESTMENT][col] 496 | + table[MarketAttribute.FEES][col] 497 | + table[MarketAttribute.MECH_FEES][col], 498 | table[MarketAttribute.EARNINGS][col], 499 | ) 500 | 501 | 502 | def _get_market_state(market: Dict[str, Any]) -> MarketState: 503 | try: 504 | now = datetime.datetime.utcnow() 505 | 506 | market_state = MarketState.CLOSED 507 | if market[ 508 | "currentAnswer" 509 | ] is None and now >= datetime.datetime.utcfromtimestamp( 510 | float(market.get("openingTimestamp", 0)) 511 | ): 512 | market_state = MarketState.PENDING 513 | elif market["currentAnswer"] is None: 514 | market_state = MarketState.OPEN 515 | elif market["isPendingArbitration"]: 516 | market_state = MarketState.ARBITRATING 517 | elif now < datetime.datetime.utcfromtimestamp( 518 | float(market.get("answerFinalizedTimestamp", 0)) 519 | ): 520 | market_state = MarketState.FINALIZING 521 | 522 | return market_state 523 | except Exception: # pylint: disable=broad-except 524 | return MarketState.UNKNOWN 525 | 526 | 527 | def _format_table(table: Dict[Any, Dict[Any, Any]]) -> str: 528 | column_width = 18 529 | 530 | table_str = " " * column_width 531 | 532 | for col in STATS_TABLE_COLS: 533 | table_str += f"{col:>{column_width}}" 534 | 535 | table_str += "\n" 536 | table_str += "-" * column_width * (len(STATS_TABLE_COLS) + 1) + "\n" 537 | 538 | table_str += ( 539 | f"{MarketAttribute.NUM_TRADES:<{column_width}}" 540 | + "".join( 541 | [ 542 | f"{table[MarketAttribute.NUM_TRADES][c]:>{column_width}}" 543 | for c in STATS_TABLE_COLS 544 | ] 545 | ) 546 | + "\n" 547 | ) 548 | table_str += ( 549 | f"{MarketAttribute.NUM_VALID_TRADES:<{column_width}}" 550 | + "".join( 551 | [ 552 | f"{table[MarketAttribute.NUM_VALID_TRADES][c]:>{column_width}}" 553 | for c in STATS_TABLE_COLS 554 | ] 555 | ) 556 | + "\n" 557 | ) 558 | table_str += ( 559 | f"{MarketAttribute.WINNER_TRADES:<{column_width}}" 560 | + "".join( 561 | [ 562 | f"{table[MarketAttribute.WINNER_TRADES][c]:>{column_width}}" 563 | for c in STATS_TABLE_COLS 564 | ] 565 | ) 566 | + "\n" 567 | ) 568 | table_str += ( 569 | f"{MarketAttribute.NUM_REDEEMED:<{column_width}}" 570 | + "".join( 571 | [ 572 | f"{table[MarketAttribute.NUM_REDEEMED][c]:>{column_width}}" 573 | for c in STATS_TABLE_COLS 574 | ] 575 | ) 576 | + "\n" 577 | ) 578 | table_str += ( 579 | f"{MarketAttribute.NUM_INVALID_MARKET:<{column_width}}" 580 | + "".join( 581 | [ 582 | f"{table[MarketAttribute.NUM_INVALID_MARKET][c]:>{column_width}}" 583 | for c in STATS_TABLE_COLS 584 | ] 585 | ) 586 | + "\n" 587 | ) 588 | table_str += ( 589 | f"{MarketAttribute.MECH_CALLS:<{column_width}}" 590 | + "".join( 591 | [ 592 | f"{table[MarketAttribute.MECH_CALLS][c]:>{column_width}}" 593 | for c in STATS_TABLE_COLS 594 | ] 595 | ) 596 | + "\n" 597 | ) 598 | table_str += ( 599 | f"{MarketAttribute.INVESTMENT:<{column_width}}" 600 | + "".join( 601 | [ 602 | f"{wei_to_xdai(table[MarketAttribute.INVESTMENT][c]):>{column_width}}" 603 | for c in STATS_TABLE_COLS 604 | ] 605 | ) 606 | + "\n" 607 | ) 608 | table_str += ( 609 | f"{MarketAttribute.FEES:<{column_width}}" 610 | + "".join( 611 | [ 612 | f"{wei_to_xdai(table[MarketAttribute.FEES][c]):>{column_width}}" 613 | for c in STATS_TABLE_COLS 614 | ] 615 | ) 616 | + "\n" 617 | ) 618 | table_str += ( 619 | f"{MarketAttribute.MECH_FEES:<{column_width}}" 620 | + "".join( 621 | [ 622 | f"{wei_to_xdai(table[MarketAttribute.MECH_FEES][c]):>{column_width}}" 623 | for c in STATS_TABLE_COLS 624 | ] 625 | ) 626 | + "\n" 627 | ) 628 | table_str += ( 629 | f"{MarketAttribute.EARNINGS:<{column_width}}" 630 | + "".join( 631 | [ 632 | f"{wei_to_xdai(table[MarketAttribute.EARNINGS][c]):>{column_width}}" 633 | for c in STATS_TABLE_COLS 634 | ] 635 | ) 636 | + "\n" 637 | ) 638 | table_str += ( 639 | f"{MarketAttribute.NET_EARNINGS:<{column_width}}" 640 | + "".join( 641 | [ 642 | f"{wei_to_xdai(table[MarketAttribute.NET_EARNINGS][c]):>{column_width}}" 643 | for c in STATS_TABLE_COLS 644 | ] 645 | ) 646 | + "\n" 647 | ) 648 | table_str += ( 649 | f"{MarketAttribute.REDEMPTIONS:<{column_width}}" 650 | + "".join( 651 | [ 652 | f"{wei_to_xdai(table[MarketAttribute.REDEMPTIONS][c]):>{column_width}}" 653 | for c in STATS_TABLE_COLS 654 | ] 655 | ) 656 | + "\n" 657 | ) 658 | table_str += ( 659 | f"{MarketAttribute.ROI:<{column_width}}" 660 | + "".join( 661 | [ 662 | f"{table[MarketAttribute.ROI][c]*100.0:>{column_width-5}.2f} % " 663 | for c in STATS_TABLE_COLS 664 | ] 665 | ) 666 | + "\n" 667 | ) 668 | 669 | return table_str 670 | 671 | 672 | def parse_user( # pylint: disable=too-many-locals,too-many-statements 673 | rpc: str, 674 | creator: str, 675 | creator_trades_json: Dict[str, Any], 676 | mech_statistics: Dict[str, Any], 677 | ) -> tuple[str, Dict[Any, Any]]: 678 | """Parse the trades from the response.""" 679 | 680 | _mech_statistics = dict(mech_statistics) 681 | user_json = _query_conditional_tokens_gc_subgraph(creator) 682 | 683 | statistics_table = { 684 | row: {col: 0 for col in STATS_TABLE_COLS} for row in STATS_TABLE_ROWS 685 | } 686 | 687 | output = "------\n" 688 | output += "Trades\n" 689 | output += "------\n" 690 | 691 | for fpmmTrade in creator_trades_json["data"]["fpmmTrades"]: 692 | try: 693 | collateral_amount = int(fpmmTrade["collateralAmount"]) 694 | outcome_index = int(fpmmTrade["outcomeIndex"]) 695 | fee_amount = int(fpmmTrade["feeAmount"]) 696 | outcomes_tokens_traded = int(fpmmTrade["outcomeTokensTraded"]) 697 | creation_timestamp = float(fpmmTrade["creationTimestamp"]) 698 | 699 | fpmm = fpmmTrade["fpmm"] 700 | 701 | output += f' Question: {fpmmTrade["title"]}\n' 702 | output += f' Market URL: https://aiomen.eth.limo/#/{fpmm["id"]}\n' 703 | 704 | creation_timestamp_utc = datetime.datetime.fromtimestamp( 705 | creation_timestamp, tz=datetime.timezone.utc 706 | ) 707 | output += f' Trade date: {creation_timestamp_utc.strftime("%Y-%m-%d %H:%M:%S %Z")}\n' 708 | 709 | market_status = _get_market_state(fpmm) 710 | 711 | statistics_table[MarketAttribute.NUM_TRADES][market_status] += 1 712 | statistics_table[MarketAttribute.INVESTMENT][ 713 | market_status 714 | ] += collateral_amount 715 | statistics_table[MarketAttribute.FEES][market_status] += fee_amount 716 | mech_data = _mech_statistics.pop(fpmmTrade["title"], {}) 717 | statistics_table[MarketAttribute.MECH_CALLS][ 718 | market_status 719 | ] += mech_data.get("count", 0) 720 | mech_fees = mech_data.get("fees", 0) 721 | statistics_table[MarketAttribute.MECH_FEES][market_status] += mech_fees 722 | 723 | output += f" Market status: {market_status}\n" 724 | output += f" Bought: {wei_to_xdai(collateral_amount)} for {wei_to_xdai(outcomes_tokens_traded)} {fpmm['outcomes'][outcome_index]!r} tokens\n" 725 | output += f" Fee: {wei_to_xdai(fee_amount)}\n" 726 | output += f" Your answer: {fpmm['outcomes'][outcome_index]!r}\n" 727 | 728 | if market_status == MarketState.FINALIZING: 729 | current_answer = int(fpmm["currentAnswer"], 16) # type: ignore 730 | is_invalid = current_answer == INVALID_ANSWER 731 | 732 | if is_invalid: 733 | earnings = collateral_amount 734 | output += "Current answer: Market has been declared invalid.\n" 735 | elif outcome_index == current_answer: 736 | earnings = outcomes_tokens_traded 737 | output += f"Current answer: {fpmm['outcomes'][current_answer]!r}\n" 738 | statistics_table[MarketAttribute.WINNER_TRADES][market_status] += 1 739 | else: 740 | earnings = 0 741 | output += f"Current answer: {fpmm['outcomes'][current_answer]!r}\n" 742 | 743 | statistics_table[MarketAttribute.EARNINGS][market_status] += earnings 744 | 745 | elif market_status == MarketState.CLOSED: 746 | current_answer = int(fpmm["currentAnswer"], 16) # type: ignore 747 | is_invalid = current_answer == INVALID_ANSWER 748 | 749 | if is_invalid: 750 | earnings = collateral_amount 751 | output += " Final answer: Market has been declared invalid.\n" 752 | output += f" Earnings: {wei_to_xdai(earnings)}\n" 753 | redeemed = _is_redeemed(user_json, fpmmTrade) 754 | if redeemed: 755 | statistics_table[MarketAttribute.NUM_INVALID_MARKET][ 756 | market_status 757 | ] += 1 758 | statistics_table[MarketAttribute.REDEMPTIONS][ 759 | market_status 760 | ] += earnings 761 | 762 | elif outcome_index == current_answer: 763 | earnings = outcomes_tokens_traded 764 | output += f" Final answer: {fpmm['outcomes'][current_answer]!r} - Congrats! The trade was for the winner answer.\n" 765 | output += f" Earnings: {wei_to_xdai(earnings)}\n" 766 | redeemed = _is_redeemed(user_json, fpmmTrade) 767 | output += f" Redeemed: {redeemed}\n" 768 | statistics_table[MarketAttribute.WINNER_TRADES][market_status] += 1 769 | 770 | if redeemed: 771 | statistics_table[MarketAttribute.NUM_REDEEMED][ 772 | market_status 773 | ] += 1 774 | statistics_table[MarketAttribute.REDEMPTIONS][ 775 | market_status 776 | ] += earnings 777 | else: 778 | earnings = 0 779 | output += f" Final answer: {fpmm['outcomes'][current_answer]!r} - The trade was for the loser answer.\n" 780 | 781 | 782 | statistics_table[MarketAttribute.EARNINGS][ 783 | market_status 784 | ] += earnings 785 | 786 | statistics_table[MarketAttribute.NUM_VALID_TRADES][ 787 | market_status 788 | ] = statistics_table[MarketAttribute.NUM_TRADES][ 789 | market_status 790 | ] - statistics_table[MarketAttribute.NUM_INVALID_MARKET][ 791 | market_status 792 | ] 793 | 794 | if 0 < earnings < DUST_THRESHOLD: 795 | output += "Earnings are dust.\n" 796 | 797 | output += "\n" 798 | except TypeError: 799 | output += "ERROR RETRIEVING TRADE INFORMATION.\n\n" 800 | 801 | output += "\n" 802 | output += "--------------------------\n" 803 | output += "Summary (per market state)\n" 804 | output += "--------------------------\n" 805 | output += "\n" 806 | 807 | # Read rpc and get safe address balance 808 | safe_address_balance = get_balance(creator, rpc) 809 | 810 | output += f"Safe address: {creator}\n" 811 | output += f"Address balance: {wei_to_xdai(safe_address_balance)}\n" 812 | 813 | wxdai_balance = get_token_balance(creator, WXDAI_CONTRACT_ADDRESS, rpc) 814 | output += f"Token balance: {wei_to_wxdai(wxdai_balance)}\n\n" 815 | 816 | _compute_totals(statistics_table, mech_statistics) 817 | output += _format_table(statistics_table) 818 | 819 | return output, statistics_table 820 | 821 | 822 | def get_mech_statistics(mech_requests: Dict[str, Any]) -> Dict[str, Dict[str, int]]: 823 | """Outputs a table with Mech statistics""" 824 | 825 | mech_statistics: Dict[str, Dict[str, int]] = defaultdict(lambda: defaultdict(int)) 826 | 827 | for mech_request in mech_requests.values(): 828 | if ( 829 | "ipfs_contents" not in mech_request 830 | or "tool" not in mech_request["ipfs_contents"] 831 | or "prompt" not in mech_request["ipfs_contents"] 832 | ): 833 | continue 834 | 835 | if mech_request["ipfs_contents"]["tool"] in IRRELEVANT_TOOLS: 836 | continue 837 | 838 | prompt = mech_request["ipfs_contents"]["prompt"] 839 | prompt = prompt.replace("\n", " ") 840 | prompt = prompt.strip() 841 | prompt = re.sub(r"\s+", " ", prompt) 842 | prompt_match = re.search(r"\"(.*)\"", prompt) 843 | if prompt_match: 844 | question = prompt_match.group(1) 845 | else: 846 | question = prompt 847 | 848 | mech_statistics[question]["count"] += 1 849 | mech_statistics[question]["fees"] += mech_request["fee"] 850 | 851 | return mech_statistics 852 | 853 | 854 | if __name__ == "__main__": 855 | user_args = _parse_args() 856 | 857 | with open(RPC_PATH, "r", encoding="utf-8") as rpc_file: 858 | rpc = rpc_file.read() 859 | 860 | mech_requests = get_mech_requests( 861 | user_args.creator, 862 | user_args.from_date.timestamp(), 863 | user_args.to_date.timestamp(), 864 | ) 865 | mech_statistics = get_mech_statistics(mech_requests) 866 | 867 | trades_json = _query_omen_xdai_subgraph( 868 | user_args.creator, 869 | user_args.from_date.timestamp(), 870 | user_args.to_date.timestamp(), 871 | user_args.fpmm_created_from_date.timestamp(), 872 | user_args.fpmm_created_to_date.timestamp(), 873 | ) 874 | parsed_output, _ = parse_user(rpc, user_args.creator, trades_json, mech_statistics) 875 | print(parsed_output) 876 | --------------------------------------------------------------------------------