├── .env.example
├── .github
└── workflows
│ ├── backend.yml
│ ├── common_checks.yml
│ ├── gitleaks.yml
│ └── release.yml
├── .gitignore
├── .gitleaks.toml
├── .gitleaksignore
├── .nvmrc
├── .pylintrc
├── LICENSE
├── README.md
├── SECURITY.md
├── api.md
├── contracts
├── ServiceRegistryTokenUtility.json
├── StakingActivityChecker.json
└── StakingToken.json
├── operate
├── __init__.py
├── account
│ ├── __init__.py
│ └── user.py
├── cli.py
├── constants.py
├── data
│ ├── __init__.py
│ └── contracts
│ │ ├── __init__.py
│ │ ├── service_staking_token
│ │ ├── __init__.py
│ │ ├── build
│ │ │ └── ServiceStakingToken.json
│ │ ├── contract.py
│ │ └── contract.yaml
│ │ └── uniswap_v2_erc20
│ │ ├── __init__.py
│ │ ├── contract.py
│ │ └── contract.yaml
├── http
│ ├── __init__.py
│ └── exceptions.py
├── keys.py
├── ledger
│ ├── __init__.py
│ ├── base.py
│ ├── ethereum.py
│ ├── profiles.py
│ └── solana.py
├── pearl.py
├── resource.py
├── services
│ ├── __init__.py
│ ├── deployment_runner.py
│ ├── health_checker.py
│ ├── manage.py
│ ├── protocol.py
│ ├── service.py
│ └── utils
│ │ ├── __init__.py
│ │ └── tendermint.py
├── types.py
├── utils
│ ├── __init__.py
│ └── gnosis.py
└── wallet
│ ├── __init__.py
│ └── master.py
├── package.json
├── poetry.lock
├── pyproject.toml
├── report.py
├── run_service.py
├── run_service.sh
├── scripts
├── __init__.py
├── fund.py
├── keys
│ ├── README.md
│ └── gnosis.txt
├── setup_wallet.py
├── test_e2e.py
├── test_staking_e2e.py
└── transfer_olas.py
├── staking_report.py
├── stop_service.py
├── stop_service.sh
├── suggest_funding_report.py
├── templates
└── superfest.yaml
├── tests
└── __init__.py
├── tox.ini
├── utils.py
├── wallet_info.py
└── yarn.lock
/.env.example:
--------------------------------------------------------------------------------
1 | FORK_URL=
2 | NODE_ENV=
3 | DEV_RPC=
4 | STAKING_TEST_KEYS_PATH=
5 | IS_STAGING=
6 |
--------------------------------------------------------------------------------
/.github/workflows/backend.yml:
--------------------------------------------------------------------------------
1 | # TBA
2 | name: Backend
3 |
4 | on: [pull_request]
5 |
6 | jobs:
7 | build:
8 | runs-on: ubuntu-latest
9 | steps:
10 |
11 | - uses: actions/checkout@v2
12 |
13 | # Node.js (for package scripts)
14 | - uses: actions/setup-node@v4
15 | with:
16 | node-version: lts/*
17 |
18 | # Python
19 | - uses: actions/setup-python@v4
20 | with:
21 | python-version: "3.10"
22 |
23 | - uses: snok/install-poetry@v1
24 | with:
25 | version: "1.7.1"
26 | virtualenvs-create: true
27 | virtualenvs-in-project: false
28 | virtualenvs-path: ~/my-custom-path
29 | installer-parallel: true
30 |
31 | # Install backend dependencies
32 | - run: yarn install:backend
33 |
34 | # Run backend
35 | # - run: yarn dev:backend
--------------------------------------------------------------------------------
/.github/workflows/common_checks.yml:
--------------------------------------------------------------------------------
1 | name: "Common Checks"
2 | on:
3 | push:
4 | branches:
5 | - develop
6 | - main
7 | pull_request:
8 | jobs:
9 | linter_checks:
10 | continue-on-error: False
11 | runs-on: ${{ matrix.os }}
12 | strategy:
13 | matrix:
14 | os: [ubuntu-latest]
15 | python-version: ["3.10.9"]
16 | timeout-minutes: 30
17 | steps:
18 | - uses: actions/checkout@v2
19 | - uses: actions/setup-python@master
20 | with:
21 | python-version: ${{ matrix.python-version }}
22 | - uses: actions/setup-go@v3
23 | with:
24 | go-version: "1.17.7"
25 | - name: Install dependencies
26 | run: |
27 | sudo apt-get update --fix-missing
28 | sudo apt-get autoremove
29 | sudo apt-get autoclean
30 | pip install tomte[tox]==0.2.15
31 | pip install --user --upgrade setuptools
32 | sudo npm install -g markdown-spellcheck
33 | - name: Security checks
34 | run: |
35 | tox -p -e bandit -e safety
36 | - name: Code style check
37 | run: |
38 | tox -p -e black-check -e isort-check
39 | - name: Flake7
40 | run: |
41 | tox -e flake8
42 | - name: Pylint
43 | run: tox -e pylint
44 | - name: Static type check
45 | run: tox -e mypy
46 | # - name: Check spelling
47 | # run: tox -e spell-check
48 | # - name: License compatibility check
49 | # run: tox -e liccheck
50 | # tox -p -e vulture -e darglint
51 |
--------------------------------------------------------------------------------
/.github/workflows/gitleaks.yml:
--------------------------------------------------------------------------------
1 | name: Gitleaks
2 | on:
3 | pull_request:
4 | push:
5 | branches:
6 | - main
7 | jobs:
8 | scan:
9 | name: gitleaks
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v3
13 | with:
14 | fetch-depth: 0
15 | - uses: actions/setup-go@v3
16 | with:
17 | go-version: "1.17.7"
18 | - run: |
19 | wget https://github.com/zricethezav/gitleaks/releases/download/v8.10.1/gitleaks_8.10.1_linux_x64.tar.gz && \
20 | tar -xzf gitleaks_8.10.1_linux_x64.tar.gz && \
21 | sudo install gitleaks /usr/bin && \
22 | gitleaks detect --report-format json --report-path leak_report -v
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | # This workflow is triggered on pushing a tag BE CAREFUL this application AUTO UPDATES !!!
4 | # git tag vX.Y.Z
5 | # git push origin tag vX.Y.Z
6 |
7 | on:
8 | push:
9 | tags:
10 | - 'v*.*.*'
11 |
12 | jobs:
13 | build-macos-pyinstaller:
14 | runs-on: ${{ matrix.os }}
15 | strategy:
16 | matrix:
17 | os: [ macos-14, macos-14-large ]
18 |
19 | steps:
20 | - uses: actions/checkout@v3
21 | - uses: actions/setup-python@v4
22 | with:
23 | python-version: '3.10'
24 |
25 | - name: Install and configure Poetry
26 | uses: snok/install-poetry@v1
27 | with:
28 | version: '1.4.0'
29 | virtualenvs-create: true
30 | virtualenvs-in-project: false
31 | virtualenvs-path: ~/my-custom-path
32 | installer-parallel: true
33 |
34 | - name: Install dependencies
35 | run: poetry install
36 |
37 | - name: Set arch environment variable for macos-latest-large
38 | if: contains(matrix.os, 'large')
39 | run: echo "OS_ARCH=x64" >> $GITHUB_ENV
40 |
41 | - name: Set arch environment variable for other macOS versions
42 | if: ${{ !contains(matrix.os, 'large') }}
43 | run: echo "OS_ARCH=arm64" >> $GITHUB_ENV
44 |
45 | - name: Get trader bin
46 | run: |
47 | trader_version=$(poetry run python -c "import yaml; config = yaml.safe_load(open('templates/trader.yaml')); print(config['service_version'])")
48 | echo $trader_version
49 | mkdir dist && curl -L -o dist/aea_bin "https://github.com/valory-xyz/trader/releases/download/${trader_version}/trader_bin_${{ env.OS_ARCH }}"
50 |
51 | - name: Build with PyInstaller
52 | run: |
53 | poetry run pyinstaller operate/services/utils/tendermint.py --onefile
54 | poetry run pyinstaller --collect-data eth_account --collect-all aea --collect-all autonomy --collect-all operate --collect-all aea_ledger_ethereum --collect-all aea_ledger_cosmos --collect-all aea_ledger_ethereum_flashbots --hidden-import aea_ledger_ethereum --hidden-import aea_ledger_cosmos --hidden-import aea_ledger_ethereum_flashbots operate/pearl.py --add-binary dist/aea_bin:. --add-binary dist/tendermint:. --onefile --name pearl_${{ env.OS_ARCH }}
55 |
56 | - name: Upload Release Assets
57 | uses: actions/upload-artifact@v2
58 | with:
59 | name: pearl_${{ env.OS_ARCH }}
60 | path: dist/pearl_${{ env.OS_ARCH }}
61 |
62 | release-operate:
63 | runs-on: macos-latest
64 | needs:
65 | - "build-macos-pyinstaller"
66 | steps:
67 | - uses: actions/checkout@v2
68 | - uses: actions/setup-python@v4
69 | with:
70 | python-version: "3.10"
71 | - uses: actions/setup-node@v4
72 | with:
73 | node-version: lts/*
74 | - name: Download artifacts
75 | uses: actions/download-artifact@v2
76 | with:
77 | name: pearl_x64
78 | path: electron/bins/
79 | - name: Download artifacts
80 | uses: actions/download-artifact@v2
81 | with:
82 | name: pearl_arm64
83 | path: electron/bins/
84 | - name: Add exec permissions
85 | run: chmod +x electron/bins/pearl_x64 && chmod +x electron/bins/pearl_arm64
86 | - uses: snok/install-poetry@v1
87 | with:
88 | version: "1.7.1"
89 | virtualenvs-create: true
90 | virtualenvs-in-project: false
91 | virtualenvs-path: ~/my-custom-path
92 | installer-parallel: true
93 | - run: yarn install-deps
94 | - name: "Build frontend with env vars"
95 | run: yarn build:frontend
96 | env:
97 | NODE_ENV: production
98 | DEV_RPC: https://rpc-gate.autonolas.tech/gnosis-rpc/
99 | IS_STAGING: ${{ github.ref != 'refs/heads/main' && 'true' || 'false' }}
100 | FORK_URL: https://rpc-gate.autonolas.tech/gnosis-rpc/
101 | - run: rm -rf /dist
102 | - name: "Build, notarize, publish"
103 | env:
104 | APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLEIDPASS }}
105 | APPLE_ID: ${{ secrets.APPLEID }}
106 | APPLETEAMID: ${{ secrets.APPLETEAMID }}
107 | CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
108 | CSC_LINK: ${{ secrets.CSC_LINK }}
109 | GH_TOKEN: ${{ secrets.github_token}}
110 | NODE_ENV: production
111 | DEV_RPC: https://rpc-gate.autonolas.tech/gnosis-rpc/
112 | FORK_URL: https://rpc-gate.autonolas.tech/gnosis-rpc/
113 | run: node build.js
114 | - name: "Build frontend with dev env vars"
115 | run: yarn build:frontend
116 | env:
117 | NODE_ENV: development
118 | DEV_RPC: https://virtual.gnosis.rpc.tenderly.co/78ca845d-2b24-44a6-9ce2-869a979e8b5b
119 | IS_STAGING: ${{ github.ref != 'refs/heads/main' && 'true' || 'false' }}
120 | FORK_URL: https://virtual.gnosis.rpc.tenderly.co/78ca845d-2b24-44a6-9ce2-869a979e8b5b
121 | - name: "Build, notarize, publish dev build"
122 | env:
123 | APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLEIDPASS }}
124 | APPLE_ID: ${{ secrets.APPLEID }}
125 | APPLETEAMID: ${{ secrets.APPLETEAMID }}
126 | CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
127 | CSC_LINK: ${{ secrets.CSC_LINK }}
128 | GH_TOKEN: ${{ secrets.github_token}}
129 | NODE_ENV: development
130 | DEV_RPC: https://virtual.gnosis.rpc.tenderly.co/78ca845d-2b24-44a6-9ce2-869a979e8b5b
131 | FORK_URL: https://virtual.gnosis.rpc.tenderly.co/78ca845d-2b24-44a6-9ce2-869a979e8b5b
132 | run: |
133 | echo "DEV_RPC=https://virtual.gnosis.rpc.tenderly.co/78ca845d-2b24-44a6-9ce2-869a979e8b5b" >> .env
134 | echo -e "FORK_URL=https://virtual.gnosis.rpc.tenderly.co/78ca845d-2b24-44a6-9ce2-869a979e8b5b" >> .env
135 | node build.js
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 | frontend/node_modules
9 |
10 | # next.js
11 | frontend/.next/
12 | frontend/out/
13 | frontend/.swc/
14 |
15 | # production
16 | frontend/build
17 |
18 | # local env files
19 | .env
20 | .env*.local
21 | .DS_STORE
22 |
23 | # python
24 | .tox/
25 | .operate/
26 | .operate*/
27 | __pycache__/
28 | data/
29 | backend/temp/
30 | backend/tmp/
31 |
32 | tmp/
33 | temp/
34 |
35 | !operate/data
36 | electron/.next
37 |
38 | dist/
39 | build/
40 |
41 | cache
42 | leak_report
43 |
44 | *.dist
45 | *.build
46 | /electron/bins/
47 |
48 | # logs
49 | *.log
50 | /.olas-modius/
51 |
--------------------------------------------------------------------------------
/.gitleaksignore:
--------------------------------------------------------------------------------
1 | ada5590acaa13a35afb62c368b13c3601e658c0c:operate/services/manage.py:generic-api-key:400
2 | ada5590acaa13a35afb62c368b13c3601e658c0c:operate/services/manage.py:generic-api-key:401
3 | ada5590acaa13a35afb62c368b13c3601e658c0c:operate/services/manage.py:generic-api-key:448
4 | ada5590acaa13a35afb62c368b13c3601e658c0c:operate/services/manage.py:generic-api-key:449
5 | ef9ec7a111816282b6185e8268a460d02329fbe4:api.md:generic-api-key:13
6 | ef9ec7a111816282b6185e8268a460d02329fbe4:api.md:generic-api-key:37
7 | 44388a82d29ce4d96e554c828c3c2c12d6ee3b8a:operate/data/contracts/service_staking_token/contract.yaml:generic-api-key:10
8 | 43bb67ace89a4a6e0eee84d3ee6495088288c528:backend/operate/data/contracts/service_staking_token/contract.yaml:generic-api-key:10
9 | 19ecb1e59813c632971658183a9f2d9d88e0614b:backend/operate/data/contracts/service_staking_token/contract.yaml:generic-api-key:10
10 | 37847b0c322a0dbc8987df526a49df70301e44d4:backend/operate/ledger/profiles.py:generic-api-key:29
11 | 6834023917760bf7875cc7c107e0c59ad7925ef4:backend/operate/ledger/profiles.py:generic-api-key:32
12 | 4e8c1c21dffd9283195052117ad4c371f770e0b2:backend/operate/ledger/profiles.py:generic-api-key:28
13 | 88115a38d3843d0f233f234816229de495bc6ece:templates/trader.yaml:generic-api-key:13
14 | 0a426251fedb8b55111455e35bffd661f4489541:backend/test.py:generic-api-key:13
15 | daf41a143aa8c483db584ba1e7222e8eafec1d3b:backend/operate.yaml:generic-api-key:13
16 | daf41a143aa8c483db584ba1e7222e8eafec1d3b:backend/controller.py:generic-api-key:201
17 | af77e930289cbc87987567bff0efc25936484df2:backend/controller.py:generic-api-key:354b04972639d66053109596d3b73a1d91688964ebb:electron/constants/publishOptions.js:github-fine-grained-pat:3
18 | b04972639d66053109596d3b73a1d91688964ebb:electron/constants/publishOptions.js:github-fine-grained-pat:3
19 | af77e930289cbc87987567bff0efc25936484df2:backend/controller.py:generic-api-key:354
20 | e7de9ce0b902ed6d68f8c5b033d044f39b08f5a1:operate/data/contracts/service_staking_token/contract.yaml:generic-api-key:10
21 | d8149e9b5b7bd6a7ed7bc1039900702f1d4f287b:operate/services/manage.py:generic-api-key:405
22 | d8149e9b5b7bd6a7ed7bc1039900702f1d4f287b:operate/services/manage.py:generic-api-key:406
23 | d8149e9b5b7bd6a7ed7bc1039900702f1d4f287b:operate/services/manage.py:generic-api-key:454
24 | d8149e9b5b7bd6a7ed7bc1039900702f1d4f287b:operate/services/manage.py:generic-api-key:455
25 | d8149e9b5b7bd6a7ed7bc1039900702f1d4f287b:operate/services/manage.py:generic-api-key:45591ec07457f69e9a29f63693ac8ef887e4b5f49f0:operate/services/manage.py:generic-api-key:454
26 | 99c0f139b037da2587708212fcf6d0e20786d0ba:operate/services/manage.py:generic-api-key:405
27 | 99c0f139b037da2587708212fcf6d0e20786d0ba:operate/services/manage.py:generic-api-key:406
28 | 99c0f139b037da2587708212fcf6d0e20786d0ba:operate/services/manage.py:generic-api-key:454
29 | 99c0f139b037da2587708212fcf6d0e20786d0ba:operate/services/manage.py:generic-api-key:455
30 | 91ec07457f69e9a29f63693ac8ef887e4b5f49f0:operate/services/manage.py:generic-api-key:454
31 | 410bea2bd02ff54da69387fe8f3b58793e09f7b0:operate/services/manage.py:generic-api-key:421
32 | 410bea2bd02ff54da69387fe8f3b58793e09f7b0:operate/services/manage.py:generic-api-key:422
33 | 58687d329c0153ddbecb381ba130ed793822b882:run_service.py:generic-api-key:329
34 | 58687d329c0153ddbecb381ba130ed793822b882:run_service.py:generic-api-key:328
35 | 58687d329c0153ddbecb381ba130ed793822b882:run_service.py:generic-api-key:3284637db8956d3ffa836ab0ca8fe1ce87d0141ab64:staking_report.py:generic-api-key:154
36 | 64128d4c3415f669a9c656ad9421be2f839f5159:run_service.py:generic-api-key:391
37 | 64128d4c3415f669a9c656ad9421be2f839f5159:run_service.py:generic-api-key:392
38 | fcdfd32ce4e7a716fe6ee3d5f15fa152abec28d3:staking_report.py:generic-api-key:153
39 | 4637db8956d3ffa836ab0ca8fe1ce87d0141ab64:staking_report.py:generic-api-key:154d7c0000fb9f7acbb90ec023d0e18d4a8844c8f81:run_service.py:generic-api-key:494
40 | d7c0000fb9f7acbb90ec023d0e18d4a8844c8f81:run_service.py:generic-api-key:495
41 | d7c0000fb9f7acbb90ec023d0e18d4a8844c8f81:run_service.py:generic-api-key:494
42 | 71254cf813cf1bd92e1a77887b1c999662c6951a:run_service.py:generic-api-key:502
43 | 71254cf813cf1bd92e1a77887b1c999662c6951a:run_service.py:generic-api-key:503
44 | d70796577da75bbc564039071e8e61f0d684a6e2:run_service.py:generic-api-key:494
45 | d70796577da75bbc564039071e8e61f0d684a6e2:run_service.py:generic-api-key:495
46 | b75eae5f67da8be3d81858ca6d21e9b7b277626e:run_service.py:generic-api-key:560
47 | b75eae5f67da8be3d81858ca6d21e9b7b277626e:run_service.py:generic-api-key:561
48 | 16781f05c56727cc178c742dea37be2e5c77a9a9:run_service.py:generic-api-key:543
49 | 16781f05c56727cc178c742dea37be2e5c77a9a9:run_service.py:generic-api-key:544
50 | 66fb9cef2ecb21d06930234596f56f4a01104c42:run_service.py:generic-api-key:322
51 | 66fb9cef2ecb21d06930234596f56f4a01104c42:run_service.py:generic-api-key:340
52 | 66fb9cef2ecb21d06930234596f56f4a01104c42:run_service.py:generic-api-key:529
53 | 66fb9cef2ecb21d06930234596f56f4a01104c42:run_service.py:generic-api-key:530
54 | b1a8a38a7a6fc0749269c8e40e1d7d16ccd3e8b0:run_service.py:generic-api-key:450
55 | b1a8a38a7a6fc0749269c8e40e1d7d16ccd3e8b0:run_service.py:generic-api-key:451
56 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | lts/*
--------------------------------------------------------------------------------
/.pylintrc:
--------------------------------------------------------------------------------
1 | [MASTER]
2 | ignore-patterns=contract.py
3 | ignore=operate/data/contracts/
4 |
5 | [MESSAGES CONTROL]
6 | disable=C0103,R0801,C0301,C0201,C0204,C0209,W1203,C0302,R1735,R1729,W0511,E0611,R0903,E1101
7 |
8 | # See here for more options: https://www.codeac.io/documentation/pylint-configuration.html
9 | R1735: use-dict-literal
10 | R1729: use-a-generator
11 | C0103: invalid-name
12 | C0201: consider-iterating-dictionary
13 | W1203: logging-fstring-interpolation
14 | C0204: bad-mcs-classmethod-argument
15 | C0209: consider-using-f-string
16 | C0301: http://pylint-messages.wikidot.com/messages:c0301 > Line too long
17 | C0302: http://pylint-messages.wikidot.com/messages:c0302 > Too many lines in module
18 | R0801: similar lines
19 | E0611: no-name-in-module
20 | R0903: Too few public methods
21 |
22 | [IMPORTS]
23 | ignored-modules=os,io,psutil
24 |
25 | [DESIGN]
26 | # min-public-methods=1
27 | max-public-methods=58
28 | # max-returns=10
29 | # max-bool-expr=7
30 | max-args=6
31 | # max-locals=31
32 | # max-statements=80
33 | max-parents=10
34 | max-branches=36
35 | max-attributes=8
36 |
37 | [REFACTORING]
38 | # max-nested-blocks=6
39 |
40 | [SPELLING]
41 | # uncomment to enable
42 | # spelling-dict=en_US
43 |
44 | # List of comma separated words that should not be checked.
45 | spelling-ignore-words=nocover,pragma,params,noqa,kwargs,str,async,json,boolean,config,pytest,args,url,tx,jsonschema,traceback,api,nosec
46 |
47 | [SIMILARITIES]
48 |
49 | # Minimum lines number of a similarity.
50 | min-similarity-lines=10
51 |
52 | # Ignore comments when computing similarities.
53 | ignore-comments=yes
54 |
55 | # Ignore docstrings when computing similarities.
56 | ignore-docstrings=yes
57 |
58 | # Ignore imports when computing similarities.
59 | ignore-imports=no
60 |
61 |
--------------------------------------------------------------------------------
/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 is deprecated 🚨
3 |
4 |
5 |
6 | This repository has been deprecated and will not be supported until further notice.
7 |
8 | 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 |
14 | Olas Modius Quickstart
15 |
16 |
17 | > :warning: **Warning**
18 | > 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.
19 | > Using this code could potentially lead to loss of funds, compromised data, or asset risk.
20 | > Exercise caution and use this code at your own risk. Please refer to the [LICENSE](./LICENSE) file for details about the terms and conditions.
21 |
22 |
23 | ## Olas Modius Agent: Functionality Overview
24 |
25 | Olas Modius is an autonomous trader that operates in the crypto ecosystem on users' behalf. It requires minimum intervention by you as an end user--it is enough that you fund the agent with initial investment assets and let it run. Modius will intelligentely invest crypto assets in DEXs on your behalf and grow your portfolio.
26 |
27 | Modius trades on Balancer over the Mode chain. If you choose to run your Modius agent with staking, in addition to trading profits Modius will also accrue rewards in Olas.
28 |
29 |
30 | **Operational Process:**
31 | Similar to its human user counterpart, Modius' time is divided in *epoch* days-periods of 24 hours. Throughout one epoch, the agent:
32 |
33 | 1. **Identifies trading opportunities** in investment pools advertised through campaigns on the Merkl platform across the supported DEXs.
34 | 2. **Grabs the most interesting one and invests in it:** Starts off by picking the most convenient investment pool amongst the ones with Annual Percentage Yield (APY) higher than 5%. Switches from one investment pool to another when the corresponding APY exceeds that of the previous investment.
35 | 3. **Tracks transactions on the staking chain to accrue Olas staking rewards:** Transactions on Mode serve as key performance indicators (KPIs) for Olas Staking Rewards; in order to collect the corresponding rewards within each epoch, Modius needs to perform a well defined (as per the staking contract) number of them.
36 |
37 |
38 | #### Notes:
39 |
40 | - Staking is currently in a testing phase, so the number of trader agents that can be staked is limited.
41 | - Within each staking period (24hrs) staking happens after the agent has reached its staking contract's KPIs. In the current agent's version, this takes approximately 45 minutes of activity.
42 | - In case a service becomes inactive and remains so for more than 2 staking periods (approx. 48 hours), it faces temporary eviction from the staking program and ceases to accrue additional rewards.
43 |
44 |
45 | ## Minimal Funding Requirements
46 |
47 | For the initial setup you will need to provide your agent with assets for gas (to cover the costs of the transactions over the operating chain) and for investment funds.
48 |
49 | - The initial gas funds amount suggested for Mode is 0.00516 ETH--based on the gas prices observed between Sept 2024 and Nov 2024 included, and may need to be revised.
50 | - The investment fund suggested is of 20 USD (about 20 USDC at the time of the writing).
51 |
52 | Additionally, if you choose to run your agent with staking, you will need a total of 40 Olas bridged to Mode.
53 |
54 |
55 | ## Compatible Systems
56 |
57 | - Windows 10/11: WSL2
58 | - Mac ARM / Intel
59 | - Linux
60 | - Raspberry Pi 4
61 |
62 | ## System Requirements
63 |
64 | Ensure your machine satisfies the requirements:
65 |
66 | - Python `==3.10`
67 | - [Poetry](https://python-poetry.org/docs/) `>=1.4.0`
68 | - [Docker Engine](https://docs.docker.com/engine/install/)
69 | - [Docker Compose](https://docs.docker.com/compose/install/)
70 |
71 |
72 |
73 |
74 | ## Running the Service: Initial setup
75 |
76 | Clone this repository locally and execute:
77 | ```bash
78 | chmod +x run_service.sh
79 | ./run_service.sh
80 | ```
81 |
82 | **Tenderly and Price Data Source**
83 |
84 | As part of its trading strategies, the agent uses Tenderly to simulate routes for bridges/swaps and pick the best available route at the moment of investment.
85 |
86 | - You will need your Tenderly API Key, account slug, and project slug. (Get your own at https://dashboard.tenderly.co/ under settings.)
87 | ```bash
88 | Please enter your Tenderly API Key:
89 | Please enter your Tenderly Account Slug:
90 | Please enter your Tenderly Project Slug:
91 | ```
92 | Refer to the Tenderly Documentation for more info https://docs.tenderly.co/account/projects.
93 |
94 | CoinGecko is used as a price source to compute the initial investment assets you will need to provide the agent with. You will be asked for your CoinGecko API Key. You can get your own at https://www.coingecko.com/.
95 | Note that, you can also press enter to skip this step. In that case, the agent will fallback to pre-computed values as per the prices observed in Nov 2024.
96 | ```bash
97 | Please enter your CoinGecko API Key. Get one at https://www.coingecko.com/:
98 | ```
99 |
100 |
101 | **Minimum investment amount**
102 |
103 | Successively you will be asked to make a decision on your traded assets limit parameter--the minimum amount to be invested in trades. A pre-computed minimum amount is pompted to you and you can opt to increase it to a value of your choice. Note that this parameter will determine the initial funds requested in a further step by the agent.
104 |
105 | ```bash
106 | The minimum investment amount is 15 USD on both USDC and ETH Tokens
107 | Do you want to increase the minimum investment amount? (y/n):
108 | ```
109 |
110 | **Staking**
111 |
112 | The agent will need your answer on staking. If you plan to run it as a non-staking agent, please answer _n_ to the question below. Otherwise, please answer _y_ and, consequently when prompted, fund your agent with the required number of bridged Olas in the target staking chain.
113 |
114 | ```bash
115 | Do you want to stake your service? (y/n):
116 | ```
117 |
118 | **Investment Activity Chain RPC**
119 |
120 | You will be asked for the corresponding RPC for your agent instance. Enter your Mode RPC of preference when you are be prompted with the following request:
121 |
122 | ```bash
123 | Please enter a Mode RPC URL:
124 | ```
125 |
126 |
127 | ## Creating a local user account
128 |
129 | When run for the first time, the agent will setup for you a password protected local account. You will be asked to enter and confirm a password as below.
130 | Please be mindful of storing it in a secure space, for future use. **Hint:** If you do not want to use a password just press Enter when asked to enter and confirm your password.
131 |
132 | ```bash
133 | Creating a new local user account...
134 | Please enter a password:
135 | Please confirm your password:
136 | Creating the main wallet...
137 | ```
138 |
139 |
140 | Finally, when prompted, send the corresponding funds for gas and investment to the corresponding address and you're good to go!
141 |
142 |
143 |
144 | ### Service is Running
145 |
146 | Once the ./run_service.sh has completed, i.e. the service is running, you can check out the live logs of your Modius agent.
147 |
148 | Check the name of your running container with:
149 |
150 | ```bash
151 | docker ps
152 | ```
153 |
154 | Copy the name which should be similar to 'optimus_abci_0', then run:
155 |
156 | ```bash
157 | docker logs [name] --follow
158 | ```
159 |
160 | To inspect the tree state transition of the current run of your agent run:
161 |
162 | ```bash
163 | poetry run autonomy analyse logs --from-dir .olas-modius/services/[service-hash]/deployment/persistent_data/logs/
164 | --agent aea_0 --fsm --reset-db
165 | ```
166 | where `[service-hash]` is the onchain representation of the agent code that you're running. You can get such hash through the following `ls` command:
167 |
168 | ```bash
169 | ls .olas-modius/services
170 | ```
171 |
172 |
173 |
174 | **Service Report**
175 | Additionally, you can execute the service report command to view a summary of the service status, balances, and anything in relation to Olas Staking and accrual of rewards:
176 |
177 | ```bash
178 | poetry run python report.py
179 | ```
180 |
181 | **Funding Report**
182 | Finally, you can execute the funding report command to check out balances for each of the chains your service is deployed onto.
183 |
184 | ```bash
185 | poetry run python suggest_funding_report.py
186 | ```
187 | The report will also give you insights on the latest observed gas prices per chain and, if necessary, suggest gas funding values to keep your agent active.
188 |
189 |
190 | **Stopping your agent**
191 | To stop your agent, use:
192 |
193 | ```bash
194 | ./stop_service.sh
195 | ```
196 |
197 |
198 |
199 | ## Update between versions
200 |
201 | Simply pull the latest script:
202 |
203 | ```bash
204 | git pull origin
205 | ```
206 |
207 | Then run again your service with:
208 |
209 | ```bash
210 | ./run_service.sh
211 | ```
212 |
213 |
214 |
215 |
216 | ## Advice for Windows users on installing Windows Subsystem for Linux version 2 (WSL2)
217 |
218 | 1. Open a **Command Prompt** terminal as an Administrator.
219 |
220 | 2. Run the following commands:
221 |
222 | ```bash
223 | dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart
224 | ```
225 |
226 | ```bash
227 | dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart
228 | ```
229 |
230 | 3. Then restart the computer.
231 |
232 | 4. Open a **Command Prompt** terminal.
233 |
234 | 5. Make WSL2 the default version by running:
235 |
236 | ```bash
237 | wsl --set-default-version 2
238 | ```
239 |
240 | 6. Install Ubuntu 22.04 by running:
241 |
242 | ```bash
243 | wsl --install -d Ubuntu-22.04
244 | ```
245 |
246 | 7. Follow the on-screen instructions and set a username and password for your Ubuntu installation.
247 |
248 | 8. Install Docker Desktop and enable the WSL 2 backend by following the instructions from Docker [here](https://docs.docker.com/desktop/wsl/).
249 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | This document outlines security procedures and general policies for the `olas-operate-app` project.
4 |
5 | ## Supported Versions
6 |
7 | The following table shows which versions of `olas-operate-app` are currently being supported with security updates.
8 |
9 | | Version | Supported |
10 | |-----------------|--------------------|
11 | | `1.0.0` | :white_check_mark: |
12 | | `< 1.0.0` | :x: |
13 |
14 | ## Reporting a Vulnerability
15 |
16 | The `olas-operate-app` team and community take all security bugs in `olas-operate-app` seriously. Thank you for improving the security of `olas-operate-app`. We appreciate your efforts and responsible disclosure and will make every effort to acknowledge your contributions.
17 |
18 | Report security bugs by emailing `info@valory.xyz`.
19 |
20 | The lead maintainer will acknowledge your email within 48 hours, and will send a more detailed response within 48 hours indicating the next steps in handling your report. After the initial reply to your report, the security team will endeavour to keep you informed of the progress towards a fix and full announcement, and may ask for additional information or guidance.
21 |
22 | Report security bugs in third-party modules to the person or team maintaining the module.
23 |
24 | ## Disclosure Policy
25 |
26 | When the security team receives a security bug report, they will assign it to a primary handler. This person will coordinate the fix and release process, involving the following steps:
27 |
28 | - Confirm the problem and determine the affected versions.
29 | - Audit code to find any potential similar problems.
30 | - Prepare fixes for all releases still under maintenance. These fixes will be released as fast as possible to PyPI.
31 |
32 | ## Comments on this Policy
33 |
34 | If you have suggestions on how this process could be improved please submit a pull request.
35 |
--------------------------------------------------------------------------------
/api.md:
--------------------------------------------------------------------------------
1 | #### `GET /api`
2 |
3 | Returns information of the operate daemon
4 |
5 |
6 | Response
7 |
8 | ```json
9 | {
10 | "name": "Operate HTTP server",
11 | "version": "0.1.0.rc0",
12 | "account": {
13 | "key": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb9226a"
14 | },
15 | "home": "/Users/virajpatel/valory/olas-operate-app/.operate"
16 | }
17 | ```
18 |
19 |
20 |
21 | ---
22 | #### `GET /api/account`
23 |
24 | Returns account status
25 |
26 |
27 | Before setup
28 |
29 | ```json
30 | {
31 | "is_setup": false
32 | }
33 | ```
34 |
35 |
36 |
37 |
38 | After setup
39 |
40 | ```json
41 | {
42 | "is_setup": true
43 | }
44 | ```
45 |
46 |
47 | ---
48 | #### `POST /api/account`
49 |
50 | Create a local user account
51 |
52 |
53 | Request
54 |
55 | ```json
56 | {
57 | "password": "Hello,World!",
58 | }
59 | ```
60 |
61 |
62 |
63 |
64 | Response
65 |
66 | ```json
67 | {
68 | "error": null
69 | }
70 | ```
71 |
72 |
73 | If account already exists
74 |
75 |
76 | Response
77 |
78 | ```json
79 | {
80 | "error": "Account already exists"
81 | }
82 | ```
83 |
84 |
85 | ---
86 | #### `PUT /api/account`
87 |
88 | Update password
89 |
90 |
91 | Request
92 |
93 | ```json
94 | {
95 | "old_password": "Hello,World!",
96 | "new_password": "Hello,World",
97 | }
98 | ```
99 |
100 |
101 |
102 |
103 | Response
104 |
105 | ```json
106 | {
107 | "error": null
108 | }
109 | ```
110 |
111 |
112 | Old password is not valid
113 |
114 |
115 | Response
116 |
117 | ```json
118 | {
119 | "error": "Old password is not valid",
120 | "traceback": "..."
121 | }
122 | ```
123 |
124 |
125 | ---
126 | #### `POST /api/account/login`
127 |
128 | Login and create a session
129 |
130 |
131 | Request
132 |
133 | ```json
134 | {
135 | "password": "Hello,World",
136 | }
137 | ```
138 |
139 |
140 |
141 |
142 | Response
143 |
144 | ```json
145 | {
146 | "message": "Login successful"
147 | }
148 | ```
149 |
150 |
151 | ---
152 | #### `GET /api/wallet`
153 |
154 | Returns a list of available wallets
155 |
156 |
157 | Response
158 |
159 | ```json
160 | [
161 | {
162 | "address": "0xFafd5cb31a611C5e5aa65ea8c6226EB4328175E7",
163 | "safe_chains": [
164 | 2
165 | ],
166 | "ledger_type": 0,
167 | "safe": "0xd56fb274ce2C66008D5c4C09980c4f36Ab81ff23",
168 | "safe_nonce": 110558881674480320952254000342160989674913430251257716940579305238321962891821
169 | }
170 | ]
171 | ```
172 |
173 |
174 | ---
175 | #### `POST /api/wallet`
176 |
177 | Creates a master key for given chain type.
178 |
179 |
180 | Request
181 |
182 | ```js
183 | {
184 | "chain_type": ChainType,
185 | }
186 | ```
187 |
188 |
189 |
190 |
191 | Response
192 |
193 | ```json
194 | {
195 | "wallet": {
196 | "address": "0xAafd5cb31a611C5e5aa65ea8c6226EB4328175E1",
197 | "safe_chains": [],
198 | "ledger_type": 0,
199 | "safe": null,
200 | "safe_nonce": null
201 | },
202 | "mnemonic": [...]
203 | }
204 | ```
205 |
206 |
207 | ---
208 | #### `PUT /api/wallet`
209 |
210 | Creates a gnosis safe for given chain type.
211 |
212 |
213 | Request
214 |
215 | ```js
216 | {
217 | "chain_type": ChainType,
218 | }
219 | ```
220 |
221 |
222 |
223 |
224 | Response
225 |
226 | ```json
227 | {
228 | "address": "0xaaFd5cb31A611C5e5aa65ea8c6226EB4328175E3",
229 | "safe_chains": [
230 | 2
231 | ],
232 | "ledger_type": 0,
233 | "safe": "0xe56fb574ce2C66008d5c4C09980c4f36Ab81ff22",
234 | "safe_nonce": 110558881674480320952254000342160989674913430251157716140571305138121962898821
235 | }
236 | ```
237 |
238 |
239 | ---
240 | #### `GET /api/services`
241 |
242 | Returns the list of services
243 |
244 |
245 | Response
246 |
247 | ```json
248 | [
249 | {
250 | "hash": "bafybeiha6dxygx2ntgjxhs6zzymgqk3s5biy3ozeqw6zuhr6yxgjlebfmq",
251 | "keys": [
252 | {
253 | "ledger": 0,
254 | "address": "0x6Db941e0e82feA3c02Ba83B20e3fB5Ea6ee539cf",
255 | "private_key": "0x34f58dcc11acec007644e49921fd81b9c8a959f651d6d66a42242a1b2dbaf4be"
256 | }
257 | ],
258 | "ledger_config": {
259 | "rpc": "http://localhost:8545",
260 | "type": 0,
261 | "chain": 2
262 | },
263 | "chain_data": {
264 | "instances": [
265 | "0x6Db941e0e82feA3c02Ba83B20e3fB5Ea6ee539cf"
266 | ],
267 | "token": 380,
268 | "multisig": "0x7F3e460Cf596E783ca490791643C0055Fa2034AC",
269 | "staked": false,
270 | "on_chain_state": 6,
271 | "user_params": {
272 | "nft": "bafybeig64atqaladigoc3ds4arltdu63wkdrk3gesjfvnfdmz35amv7faq",
273 | "agent_id": 14,
274 | "threshold": 1,
275 | "use_staking": false,
276 | "cost_of_bond": 10000000000000000,
277 | "olas_cost_of_bond": 10000000000000000000,
278 | "olas_required_to_stake": 10000000000000000000,
279 | "fund_requirements": {
280 | "agent": 0.1,
281 | "safe": 0.5
282 | }
283 | }
284 | },
285 | "path": "/Users/virajpatel/valory/olas-operate-app/.operate/services/bafybeiha6dxygx2ntgjxhs6zzymgqk3s5biy3ozeqw6zuhr6yxgjlebfmq",
286 | "service_path": "/Users/virajpatel/valory/olas-operate-app/.operate/services/bafybeiha6dxygx2ntgjxhs6zzymgqk3s5biy3ozeqw6zuhr6yxgjlebfmq/trader_omen_gnosis",
287 | "name": "valory/trader_omen_gnosis"
288 | }
289 | ]
290 | ```
291 |
292 |
293 |
294 | ---
295 | #### `POST /api/services`
296 |
297 | Create a service using the service template
298 |
299 |
300 | Request
301 |
302 | ```json
303 | {
304 | "name": "Trader Agent",
305 | "description": "Trader agent for omen prediction markets",
306 | "hash": "bafybeiha6dxygx2ntgjxhs6zzymgqk3s5biy3ozeqw6zuhr6yxgjlebfmq",
307 | "image": "https://operate.olas.network/_next/image?url=%2Fimages%2Fprediction-agent.png&w=3840&q=75",
308 | "configuration": {
309 | "nft": "bafybeig64atqaladigoc3ds4arltdu63wkdrk3gesjfvnfdmz35amv7faq",
310 | "rpc": "http://localhost:8545",
311 | "agent_id": 14,
312 | "threshold": 1,
313 | "use_staking": false,
314 | "cost_of_bond": 10000000000000000,
315 | "olas_cost_of_bond": 10000000000000000000,
316 | "olas_required_to_stake": 10000000000000000000,
317 | "fund_requirements": {
318 | "agent": 0.1,
319 | "safe": 0.5
320 | }
321 | }
322 | }
323 | ```
324 |
325 |
326 |
327 | Optionally you can add `deploy` parameter and set it to `true` for a full deployment in a single request.
328 |
329 |
330 | Response
331 |
332 | ```json
333 | {
334 | "hash": "bafybeiha6dxygx2ntgjxhs6zzymgqk3s5biy3ozeqw6zuhr6yxgjlebfmq",
335 | "keys": [
336 | {
337 | "ledger": 0,
338 | "address": "0x10EB940024913dfCAE95D21E04Ba662cdfB79fF0",
339 | "private_key": "0x00000000000000000000000000000000000000000000000000000000000000000"
340 | }
341 | ],
342 | "ledger_config": {
343 | "rpc": "http: //localhost:8545",
344 | "type": 0,
345 | "chain": 2
346 | },
347 | "chain_data": {
348 | "instances": [
349 | "0x10EB940024913dfCAE95D21E04Ba662cdfB79fF0"
350 | ],
351 | "token": 382,
352 | "multisig": "0xf21d8A424e83BBa2588306D1C574FE695AD410b5",
353 | "staked": false,
354 | "on_chain_state": 4,
355 | "user_params": {
356 | "nft": "bafybeig64atqaladigoc3ds4arltdu63wkdrk3gesjfvnfdmz35amv7faq",
357 | "agent_id": 14,
358 | "threshold": 1,
359 | "use_staking": false,
360 | "cost_of_bond": 10000000000000000,
361 | "olas_cost_of_bond": 10000000000000000000,
362 | "olas_required_to_stake": 10000000000000000000,
363 | "fund_requirements": {
364 | "agent": 0.1,
365 | "safe": 0.5
366 | }
367 | }
368 | },
369 | "path": "~/.operate/services/bafybeiha6dxygx2ntgjxhs6zzymgqk3s5biy3ozeqw6zuhr6yxgjlebfmq",
370 | "service_path": "~/.operate/services/bafybeiha6dxygx2ntgjxhs6zzymgqk3s5biy3ozeqw6zuhr6yxgjlebfmq/trader_omen_gnosis",
371 | "name": "valory/trader_omen_gnosis"
372 | }
373 | ```
374 |
375 |
376 |
377 | ---
378 | #### `PUT /api/services`
379 |
380 | Update a service
381 |
382 |
383 | Request
384 |
385 | ```json
386 | {
387 | "old_service_hash": "bafybeiha6dxygx2ntgjxhs6zzymgqk3s5biy3ozeqw6zuhr6yxgjlebfmq",
388 | "new_service_hash": "bafybeicxdpkuk5z5zfbkso7v5pywf4v7chxvluyht7dtgalg6dnhl7ejoe",
389 | }
390 | ```
391 |
392 |
393 |
394 | Optionally you can add `deploy` parameter and set it to `true` for a full deployment in a single request.
395 |
396 |
397 | Response
398 |
399 | ```json
400 | {
401 | "hash": "bafybeicxdpkuk5z5zfbkso7v5pywf4v7chxvluyht7dtgalg6dnhl7ejoe",
402 | "keys": [
403 | {
404 | "ledger": 0,
405 | "address": "0x10EB940024913dfCAE95D21E04Ba662cdfB79fF0",
406 | "private_key": "0x00000000000000000000000000000000000000000000000000000000000000000"
407 | }
408 | ],
409 | "ledger_config": {
410 | "rpc": "http: //localhost:8545",
411 | "type": 0,
412 | "chain": 2
413 | },
414 | "chain_data": {
415 | "instances": [
416 | "0x10EB940024913dfCAE95D21E04Ba662cdfB79fF0"
417 | ],
418 | "token": 382,
419 | "multisig": "0xf21d8A424e83BBa2588306D1C574FE695AD410b5",
420 | "staked": false,
421 | "on_chain_state": 4,
422 | "user_params": {
423 | "nft": "bafybeig64atqaladigoc3ds4arltdu63wkdrk3gesjfvnfdmz35amv7faq",
424 | "agent_id": 14,
425 | "threshold": 1,
426 | "use_staking": false,
427 | "cost_of_bond": 10000000000000000,
428 | "olas_cost_of_bond": 10000000000000000000,
429 | "olas_required_to_stake": 10000000000000000000,
430 | "fund_requirements": {
431 | "agent": 0.1,
432 | "safe": 0.5
433 | }
434 | }
435 | },
436 | "path": "~/.operate/services/bafybeicxdpkuk5z5zfbkso7v5pywf4v7chxvluyht7dtgalg6dnhl7ejoe",
437 | "service_path": "~/.operate/services/bafybeicxdpkuk5z5zfbkso7v5pywf4v7chxvluyht7dtgalg6dnhl7ejoe/trader_omen_gnosis",
438 | "name": "valory/trader_omen_gnosis"
439 | }
440 | ```
441 |
442 |
443 |
444 | ---
445 | #### `GET /api/services/{service}`
446 |
447 |
448 | Response
449 |
450 | ```json
451 | {
452 | "hash": "{service}",
453 | "keys": [
454 | {
455 | "ledger": 0,
456 | "address": "0x10EB940024913dfCAE95D21E04Ba662cdfB79fF0",
457 | "private_key": "0x00000000000000000000000000000000000000000000000000000000000000000"
458 | }
459 | ],
460 | "ledger_config": {
461 | "rpc": "http: //localhost:8545",
462 | "type": 0,
463 | "chain": 2
464 | },
465 | "chain_data": {
466 | "instances": [
467 | "0x10EB940024913dfCAE95D21E04Ba662cdfB79fF0"
468 | ],
469 | "token": 382,
470 | "multisig": "0xf21d8A424e83BBa2588306D1C574FE695AD410b5",
471 | "staked": false,
472 | "on_chain_state": 4,
473 | "user_params": {
474 | "nft": "bafybeig64atqaladigoc3ds4arltdu63wkdrk3gesjfvnfdmz35amv7faq",
475 | "agent_id": 14,
476 | "threshold": 1,
477 | "use_staking": false,
478 | "cost_of_bond": 10000000000000000,
479 | "olas_cost_of_bond": 10000000000000000000,
480 | "olas_required_to_stake": 10000000000000000000,
481 | "fund_requirements": {
482 | "agent": 0.1,
483 | "safe": 0.5
484 | }
485 | }
486 | },
487 | "path": "~/.operate/services/{service}",
488 | "service_path": "~/.operate/services/{service}/trader_omen_gnosis",
489 | "name": "valory/trader_omen_gnosis"
490 | }
491 | ```
492 |
493 |
494 |
495 | ---
496 | #### `POST /api/services/{service}/onchain/deploy`
497 |
498 | Deploy service on-chain
499 |
500 |
501 | Request
502 |
503 | ```json
504 | ```
505 |
506 |
507 |
508 |
509 | Response
510 |
511 | ```json
512 | ```
513 |
514 |
515 |
516 | ---
517 | #### `POST /api/services/{service}/onchain/stop`
518 |
519 | Stop service on-chain
520 |
521 |
522 | Request
523 |
524 | ```json
525 | ```
526 |
527 |
528 |
529 |
530 | Response
531 |
532 | ```json
533 | ```
534 |
535 |
536 |
537 | ---
538 | #### `GET /api/services/{service}/deployment`
539 |
540 |
541 | Response
542 |
543 | ```json
544 | {
545 | "status": 1,
546 | "nodes": {
547 | "agent": [
548 | "traderomengnosis_abci_0"
549 | ],
550 | "tendermint": [
551 | "traderomengnosis_tm_0"
552 | ]
553 | }
554 | }
555 | ```
556 |
557 |
558 |
559 | ---
560 | #### `POST /api/services/{service}/deployment/build`
561 |
562 | Build service locally
563 |
564 |
565 | Request
566 |
567 | ```json
568 | ```
569 |
570 |
571 |
572 |
573 | Response
574 |
575 | ```json
576 | ```
577 |
578 |
579 |
580 | ---
581 | #### `POST /api/services/{service}/deployment/start`
582 |
583 | Start agent
584 |
585 |
586 | Request
587 |
588 | ```json
589 | ```
590 |
591 |
592 |
593 |
594 | Response
595 |
596 | ```json
597 | ```
598 |
599 |
600 |
601 | ---
602 | #### `POST /api/services/{service}/deployment/stop`
603 |
604 | Stop agent
605 |
606 | ```json
607 | ```
608 |
609 | ---
610 | #### `POST /api/services/{service}/deployment/delete`
611 |
612 | Delete local deployment
613 |
614 |
615 | Request
616 |
617 | ```json
618 | ```
619 |
620 |
621 |
622 |
623 | Response
624 |
625 | ```json
626 | ```
627 |
628 |
629 |
630 |
648 |
649 |
650 |
--------------------------------------------------------------------------------
/contracts/StakingActivityChecker.json:
--------------------------------------------------------------------------------
1 | {
2 | "_format": "hh-sol-artifact-1",
3 | "contractName": "StakingActivityChecker",
4 | "sourceName": "contracts/staking/StakingActivityChecker.sol",
5 | "abi": [
6 | {
7 | "inputs": [
8 | {
9 | "internalType": "uint256",
10 | "name": "_livenessRatio",
11 | "type": "uint256"
12 | }
13 | ],
14 | "stateMutability": "nonpayable",
15 | "type": "constructor"
16 | },
17 | {
18 | "inputs": [],
19 | "name": "ZeroValue",
20 | "type": "error"
21 | },
22 | {
23 | "inputs": [
24 | {
25 | "internalType": "address",
26 | "name": "multisig",
27 | "type": "address"
28 | }
29 | ],
30 | "name": "getMultisigNonces",
31 | "outputs": [
32 | {
33 | "internalType": "uint256[]",
34 | "name": "nonces",
35 | "type": "uint256[]"
36 | }
37 | ],
38 | "stateMutability": "view",
39 | "type": "function"
40 | },
41 | {
42 | "inputs": [
43 | {
44 | "internalType": "uint256[]",
45 | "name": "curNonces",
46 | "type": "uint256[]"
47 | },
48 | {
49 | "internalType": "uint256[]",
50 | "name": "lastNonces",
51 | "type": "uint256[]"
52 | },
53 | {
54 | "internalType": "uint256",
55 | "name": "ts",
56 | "type": "uint256"
57 | }
58 | ],
59 | "name": "isRatioPass",
60 | "outputs": [
61 | {
62 | "internalType": "bool",
63 | "name": "ratioPass",
64 | "type": "bool"
65 | }
66 | ],
67 | "stateMutability": "view",
68 | "type": "function"
69 | },
70 | {
71 | "inputs": [],
72 | "name": "livenessRatio",
73 | "outputs": [
74 | {
75 | "internalType": "uint256",
76 | "name": "",
77 | "type": "uint256"
78 | }
79 | ],
80 | "stateMutability": "view",
81 | "type": "function"
82 | }
83 | ],
84 | "bytecode": "0x608060405234801561000f575f80fd5b506004361061003f575f3560e01c8063184023a514610043578063592cf3fb1461006b578063d564c4bf146100a0575b5f80fd5b610056610051366004610328565b6100c0565b60405190151581526020015b60405180910390f35b6100927f00000000000000000000000000000000000000000000000000002a1b324b8f6881565b604051908152602001610062565b6100b36100ae366004610390565b610190565b60405161006291906103c3565b5f80821180156101015750825f815181106100dd576100dd610406565b6020026020010151845f815181106100f7576100f7610406565b6020026020010151115b15610189575f82845f8151811061011a5761011a610406565b6020026020010151865f8151811061013457610134610406565b60200260200101516101469190610460565b61015890670de0b6b3a7640000610479565b6101629190610490565b7f00000000000000000000000000000000000000000000000000002a1b324b8f6811159150505b9392505050565b604080516001808252818301909252606091602080830190803683370190505090508173ffffffffffffffffffffffffffffffffffffffff1663affed0e06040518163ffffffff1660e01b8152600401602060405180830381865afa1580156101fb573d5f803e3d5ffd5b505050506040513d601f19601f8201168201806040525081019061021f91906104c8565b815f8151811061023157610231610406565b602002602001018181525050919050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b5f82601f83011261027e575f80fd5b8135602067ffffffffffffffff8083111561029b5761029b610242565b8260051b6040517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0603f830116810181811084821117156102de576102de610242565b60405293845260208187018101949081019250878511156102fd575f80fd5b6020870191505b8482101561031d57813583529183019190830190610304565b979650505050505050565b5f805f6060848603121561033a575f80fd5b833567ffffffffffffffff80821115610351575f80fd5b61035d8783880161026f565b94506020860135915080821115610372575f80fd5b5061037f8682870161026f565b925050604084013590509250925092565b5f602082840312156103a0575f80fd5b813573ffffffffffffffffffffffffffffffffffffffff81168114610189575f80fd5b602080825282518282018190525f9190848201906040850190845b818110156103fa578351835292840192918401916001016103de565b50909695505050505050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52603260045260245ffd5b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b8181038181111561047357610473610433565b92915050565b808202811582820484141761047357610473610433565b5f826104c3577f4e487b71000000000000000000000000000000000000000000000000000000005f52601260045260245ffd5b500490565b5f602082840312156104d8575f80fd5b505191905056fea26469706673582212205cd6cde3d106d475558793ccc0670f4af07e2e69e437eff007791837f35d016464736f6c63430008190033",
85 | "linkReferences": {},
86 | "deployedLinkReferences": {}
87 | }
--------------------------------------------------------------------------------
/operate/__init__.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 | """Operate app."""
21 |
--------------------------------------------------------------------------------
/operate/account/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # ------------------------------------------------------------------------------
3 | #
4 | # Copyright 2024 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 | """User account."""
21 |
--------------------------------------------------------------------------------
/operate/account/user.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # ------------------------------------------------------------------------------
3 | #
4 | # Copyright 2024 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 | """User account implementation."""
21 |
22 | import hashlib
23 | from dataclasses import dataclass
24 | from pathlib import Path
25 |
26 | from operate.resource import LocalResource
27 |
28 |
29 | def sha256(string: str) -> str:
30 | """Get SHA256 hexdigest of a string."""
31 | sh256 = hashlib.sha256()
32 | sh256.update(string.encode())
33 | return sh256.hexdigest()
34 |
35 |
36 | @dataclass
37 | class UserAccount(LocalResource):
38 | """User account."""
39 |
40 | password_sha: str
41 | path: Path
42 |
43 | @classmethod
44 | def load(cls, path: Path) -> "UserAccount":
45 | """Load user account."""
46 | return super().load(path) # type: ignore
47 |
48 | @classmethod
49 | def new(cls, password: str, path: Path) -> "UserAccount":
50 | """Create a new user."""
51 | user = UserAccount(
52 | password_sha=sha256(string=password),
53 | path=path,
54 | )
55 | user.store()
56 | return UserAccount.load(path=path)
57 |
58 | def is_valid(self, password: str) -> bool:
59 | """Check if a password string is valid."""
60 | return sha256(string=password) == self.password_sha
61 |
62 | def update(self, old_password: str, new_password: str) -> None:
63 | """Update current password."""
64 | if not self.is_valid(password=old_password):
65 | raise ValueError("Old password is not valid")
66 | self.password_sha = sha256(string=new_password)
67 | self.store()
68 |
--------------------------------------------------------------------------------
/operate/constants.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 | """Constants."""
21 |
22 | OPERATE = ".operate"
23 | CONFIG = "config.json"
24 | SERVICES = "services"
25 | KEYS = "keys"
26 | DEPLOYMENT = "deployment"
27 | DEPLOYMENT_JSON = "deployment.json"
28 | CONFIG = "config.json"
29 | KEY = "key"
30 | KEYS = "keys"
31 | KEYS_JSON = "keys.json"
32 | DOCKER_COMPOSE_YAML = "docker-compose.yaml"
33 | SERVICE_YAML = "service.yaml"
34 |
35 | ON_CHAIN_INTERACT_TIMEOUT = 120.0
36 | ON_CHAIN_INTERACT_RETRIES = 40
37 | ON_CHAIN_INTERACT_SLEEP = 3.0
38 |
--------------------------------------------------------------------------------
/operate/data/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # ------------------------------------------------------------------------------
3 | #
4 | # Copyright 2024 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 | """Data module."""
21 |
22 | from pathlib import Path
23 |
24 |
25 | DATA_DIR = Path(__file__).parent
26 |
--------------------------------------------------------------------------------
/operate/data/contracts/__init__.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 | """Contract packages."""
21 |
--------------------------------------------------------------------------------
/operate/data/contracts/service_staking_token/__init__.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 module contains the support resources for the agent registry contract."""
21 |
--------------------------------------------------------------------------------
/operate/data/contracts/service_staking_token/contract.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # ------------------------------------------------------------------------------
3 | #
4 | # Copyright 2023-2024 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 module contains the class to connect to the `ServiceStakingTokenMechUsage` contract."""
21 |
22 | from enum import Enum
23 |
24 | from aea.common import JSONLike
25 | from aea.configurations.base import PublicId
26 | from aea.contracts.base import Contract
27 | from aea.crypto.base import LedgerApi
28 |
29 |
30 | class StakingState(Enum):
31 | """Staking state enumeration for the staking."""
32 |
33 | UNSTAKED = 0
34 | STAKED = 1
35 | EVICTED = 2
36 |
37 |
38 | class ServiceStakingTokenContract(Contract):
39 | """The Service Staking contract."""
40 |
41 | contract_id = PublicId.from_str("valory/service_staking_token:0.1.0")
42 |
43 | @classmethod
44 | def get_service_staking_state(
45 | cls,
46 | ledger_api: LedgerApi,
47 | contract_address: str,
48 | service_id: int,
49 | ) -> JSONLike:
50 | """Check whether the service is staked."""
51 | contract_instance = cls.get_instance(ledger_api, contract_address)
52 | res = contract_instance.functions.getStakingState(service_id).call()
53 | return dict(data=StakingState(res))
54 |
55 | @classmethod
56 | def build_stake_tx(
57 | cls,
58 | ledger_api: LedgerApi,
59 | contract_address: str,
60 | service_id: int,
61 | ) -> JSONLike:
62 | """Build stake tx."""
63 | contract_instance = cls.get_instance(ledger_api, contract_address)
64 | data = contract_instance.encodeABI("stake", args=[service_id])
65 | return dict(data=bytes.fromhex(data[2:]))
66 |
67 | @classmethod
68 | def build_checkpoint_tx(
69 | cls,
70 | ledger_api: LedgerApi,
71 | contract_address: str,
72 | ) -> JSONLike:
73 | """Build checkpoint tx."""
74 | contract_instance = cls.get_instance(ledger_api, contract_address)
75 | data = contract_instance.encodeABI("checkpoint")
76 | return dict(data=bytes.fromhex(data[2:]))
77 |
78 | @classmethod
79 | def build_unstake_tx(
80 | cls,
81 | ledger_api: LedgerApi,
82 | contract_address: str,
83 | service_id: int,
84 | ) -> JSONLike:
85 | """Build unstake tx."""
86 | contract_instance = cls.get_instance(ledger_api, contract_address)
87 | data = contract_instance.encodeABI("unstake", args=[service_id])
88 | return dict(data=bytes.fromhex(data[2:]))
89 |
90 | @classmethod
91 | def available_rewards(
92 | cls,
93 | ledger_api: LedgerApi,
94 | contract_address: str,
95 | ) -> JSONLike:
96 | """Get the available rewards."""
97 | contract_instance = cls.get_instance(ledger_api, contract_address)
98 | res = contract_instance.functions.availableRewards().call()
99 | return dict(data=res)
100 |
101 | @classmethod
102 | def get_staking_rewards(
103 | cls,
104 | ledger_api: LedgerApi,
105 | contract_address: str,
106 | service_id: int,
107 | ) -> JSONLike:
108 | """Get the service's staking rewards."""
109 | contract = cls.get_instance(ledger_api, contract_address)
110 | reward = contract.functions.calculateStakingReward(service_id).call()
111 | return dict(data=reward)
112 |
113 | @classmethod
114 | def get_next_checkpoint_ts(
115 | cls,
116 | ledger_api: LedgerApi,
117 | contract_address: str,
118 | ) -> JSONLike:
119 | """Get the next checkpoint's timestamp."""
120 | contract = cls.get_instance(ledger_api, contract_address)
121 | ts = contract.functions.getNextRewardCheckpointTimestamp().call()
122 | return dict(data=ts)
123 |
124 | @classmethod
125 | def get_liveness_period(
126 | cls,
127 | ledger_api: LedgerApi,
128 | contract_address: str,
129 | ) -> JSONLike:
130 | """Retrieve the liveness period."""
131 | contract = cls.get_instance(ledger_api, contract_address)
132 | liveness_period = contract.functions.livenessPeriod().call()
133 | return dict(data=liveness_period)
134 |
135 | @classmethod
136 | def get_service_info(
137 | cls,
138 | ledger_api: LedgerApi,
139 | contract_address: str,
140 | service_id: int,
141 | ) -> JSONLike:
142 | """Retrieve the service info for a service."""
143 | contract = cls.get_instance(ledger_api, contract_address)
144 | info = contract.functions.getServiceInfo(service_id).call()
145 | return dict(data=info)
146 |
147 | @classmethod
148 | def max_num_services(
149 | cls,
150 | ledger_api: LedgerApi,
151 | contract_address: str,
152 | ) -> JSONLike:
153 | """Retrieve the max number of services."""
154 | contract = cls.get_instance(ledger_api, contract_address)
155 | max_num_services = contract.functions.maxNumServices().call()
156 | return dict(data=max_num_services)
157 |
158 | @classmethod
159 | def get_service_ids(
160 | cls,
161 | ledger_api: LedgerApi,
162 | contract_address: str,
163 | ) -> JSONLike:
164 | """Retrieve the service IDs."""
165 | contract = cls.get_instance(ledger_api, contract_address)
166 | service_ids = contract.functions.getServiceIds().call()
167 | return dict(data=service_ids)
168 |
169 | @classmethod
170 | def get_min_staking_duration(
171 | cls,
172 | ledger_api: LedgerApi,
173 | contract_address: str,
174 | ) -> JSONLike:
175 | """Retrieve the service IDs."""
176 | contract = cls.get_instance(ledger_api, contract_address)
177 | duration = contract.functions.minStakingDuration().call()
178 | return dict(data=duration)
179 |
--------------------------------------------------------------------------------
/operate/data/contracts/service_staking_token/contract.yaml:
--------------------------------------------------------------------------------
1 | name: service_staking_token
2 | author: valory
3 | version: 0.1.0
4 | type: contract
5 | description: Service staking token contract
6 | license: Apache-2.0
7 | aea_version: '>=1.0.0, <2.0.0'
8 | fingerprint:
9 | __init__.py: bafybeid3wfzglolebuo6jrrsopswzu4lk77bm76mvw3euizlsjtnt3wmgu
10 | build/ServiceStakingToken.json: bafybeifptgh2dpse3l7lscjdvy24t7svqnflj3n2txehu7zerdeaszldsi
11 | contract.py: bafybeiff62bquzeisbd6iptqdjetrhlkt2ut5d7j6md2kqinrqgmbveceu
12 | fingerprint_ignore_patterns: []
13 | contracts: []
14 | class_name: ServiceStakingTokenContract
15 | contract_interface_paths:
16 | ethereum: build/ServiceStakingToken.json
17 | dependencies:
18 | open-aea-ledger-ethereum:
19 | version: ==1.42.0
20 | open-aea-test-autonomy:
21 | version: ==0.13.6
22 | web3:
23 | version: <7,>=6.0.0
24 |
--------------------------------------------------------------------------------
/operate/data/contracts/uniswap_v2_erc20/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # ------------------------------------------------------------------------------
3 | #
4 | # Copyright 2021 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 module contains the support resources for the Uniswap V2 ERC20 contract."""
21 |
--------------------------------------------------------------------------------
/operate/data/contracts/uniswap_v2_erc20/contract.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # ------------------------------------------------------------------------------
3 | #
4 | # Copyright 2021-2022 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 module contains the class to connect to a ERC20 contract."""
21 | import logging
22 | from typing import Any, List, Optional, cast
23 |
24 | from aea.common import JSONLike
25 | from aea.configurations.base import PublicId
26 | from aea.contracts.base import Contract
27 | from aea_ledger_ethereum import EthereumApi
28 |
29 |
30 | PUBLIC_ID = PublicId.from_str("valory/uniswap_v2_erc20:0.1.0")
31 |
32 | _logger = logging.getLogger(
33 | f"aea.packages.{PUBLIC_ID.author}.contracts.{PUBLIC_ID.name}.contract"
34 | )
35 |
36 |
37 | # pylint: disable=too-many-arguments,invalid-name
38 | class UniswapV2ERC20Contract(Contract):
39 | """The Uniswap V2 ERC-20 contract."""
40 |
41 | contract_id = PUBLIC_ID
42 |
43 | @classmethod
44 | def approve(
45 | cls,
46 | ledger_api: EthereumApi,
47 | contract_address: str,
48 | spender_address: str,
49 | value: int,
50 | **kwargs: Any,
51 | ) -> Optional[JSONLike]:
52 | """Set the allowance for spender_address on behalf of sender_address."""
53 | contract_instance = cls.get_instance(ledger_api, contract_address)
54 |
55 | return ledger_api.build_transaction(
56 | contract_instance,
57 | "approve",
58 | method_args=dict(
59 | spender=spender_address,
60 | value=value,
61 | ),
62 | tx_args=kwargs,
63 | )
64 |
65 | @classmethod
66 | def transfer(
67 | cls,
68 | ledger_api: EthereumApi,
69 | contract_address: str,
70 | to_address: str,
71 | value: int,
72 | **tx_args: Any,
73 | ) -> Optional[JSONLike]:
74 | """Transfer funds from sender_address to to_address."""
75 | contract_instance = cls.get_instance(ledger_api, contract_address)
76 |
77 | return ledger_api.build_transaction(
78 | contract_instance,
79 | "transfer",
80 | method_args={
81 | "to": to_address,
82 | "value": value,
83 | },
84 | tx_args=tx_args,
85 | )
86 |
87 | @classmethod
88 | def transfer_from(
89 | cls,
90 | ledger_api: EthereumApi,
91 | contract_address: str,
92 | from_address: str,
93 | to_address: str,
94 | value: int,
95 | **tx_args: Any,
96 | ) -> Optional[JSONLike]:
97 | """As sender_address (third-party) transfer funds from from_address to to_address."""
98 | contract_instance = cls.get_instance(ledger_api, contract_address)
99 |
100 | return ledger_api.build_transaction(
101 | contract_instance,
102 | "transferFrom",
103 | method_args={
104 | "from": from_address,
105 | "to": to_address,
106 | "value": value,
107 | },
108 | tx_args=tx_args,
109 | )
110 |
111 | @classmethod
112 | def permit(
113 | cls,
114 | ledger_api: EthereumApi,
115 | contract_address: str,
116 | owner_address: str,
117 | spender_address: str,
118 | value: int,
119 | deadline: int,
120 | v: int,
121 | r: bytes,
122 | s: bytes,
123 | **kwargs: Any,
124 | ) -> Optional[JSONLike]:
125 | """Sets the allowance for a spender on behalf of owner where approval is granted via a signature. Sender can differ from owner."""
126 | contract_instance = cls.get_instance(ledger_api, contract_address)
127 |
128 | return ledger_api.build_transaction(
129 | contract_instance,
130 | "permit",
131 | method_args=dict(
132 | owner=owner_address,
133 | spender=spender_address,
134 | value=value,
135 | deadline=deadline,
136 | v=v,
137 | r=r,
138 | s=s,
139 | ),
140 | tx_args=kwargs,
141 | )
142 |
143 | @classmethod
144 | def allowance(
145 | cls,
146 | ledger_api: EthereumApi,
147 | contract_address: str,
148 | owner_address: str,
149 | spender_address: str,
150 | ) -> Optional[JSONLike]:
151 | """Gets the allowance for a spender."""
152 | contract_instance = cls.get_instance(ledger_api, contract_address)
153 |
154 | return ledger_api.contract_method_call(
155 | contract_instance=contract_instance,
156 | method_name="allowance",
157 | owner=owner_address,
158 | spender=spender_address,
159 | )
160 |
161 | @classmethod
162 | def balance_of(
163 | cls, ledger_api: EthereumApi, contract_address: str, owner_address: str
164 | ) -> Optional[JSONLike]:
165 | """Gets an account's balance."""
166 | contract_instance = cls.get_instance(ledger_api, contract_address)
167 |
168 | return ledger_api.contract_method_call(
169 | contract_instance=contract_instance,
170 | method_name="balanceOf",
171 | owner=owner_address,
172 | )
173 |
174 | @classmethod
175 | def get_transaction_transfer_logs( # type: ignore # pylint: disable=too-many-arguments,too-many-locals,unused-argument,arguments-differ
176 | cls,
177 | ledger_api: EthereumApi,
178 | contract_address: str,
179 | tx_hash: str,
180 | target_address: Optional[str] = None,
181 | ) -> JSONLike:
182 | """
183 | Get all transfer events derived from a transaction.
184 |
185 | :param ledger_api: the ledger API object
186 | :param contract_address: the address of the contract
187 | :param tx_hash: the transaction hash
188 | :param target_address: optional address to filter tranfer events to just those that affect it
189 | :return: the verified status
190 | """
191 | transfer_logs_data: Optional[JSONLike] = super(
192 | UniswapV2ERC20Contract, cls
193 | ).get_transaction_transfer_logs( # type: ignore
194 | ledger_api,
195 | tx_hash,
196 | target_address,
197 | )
198 | transfer_logs: List = []
199 |
200 | if transfer_logs_data:
201 | transfer_logs = cast(
202 | List,
203 | transfer_logs_data["logs"],
204 | )
205 |
206 | transfer_logs = [
207 | {
208 | "from": log["args"]["from"],
209 | "to": log["args"]["to"],
210 | "value": log["args"]["value"],
211 | "token_address": log["address"],
212 | }
213 | for log in transfer_logs
214 | ]
215 |
216 | if target_address:
217 | transfer_logs = list(
218 | filter(
219 | lambda log: target_address in (log["from"], log["to"]), # type: ignore
220 | transfer_logs,
221 | )
222 | )
223 |
224 | return dict(logs=transfer_logs)
225 |
--------------------------------------------------------------------------------
/operate/data/contracts/uniswap_v2_erc20/contract.yaml:
--------------------------------------------------------------------------------
1 | name: uniswap_v2_erc20
2 | author: valory
3 | version: 0.1.0
4 | type: contract
5 | description: Uniswap V2 ERC20 contract
6 | license: Apache-2.0
7 | aea_version: '>=1.0.0, <2.0.0'
8 | fingerprint:
9 | __init__.py: bafybeia75xbvf6zogcloppdcu2zrwcl6d46ebj2zmm6dkge6eno3k6ysw4
10 | build/IUniswapV2ERC20.json: bafybeid45gy6x5ah5ub5p2obdhph36skpoy5qdhikwwfso7f3655iqnpc4
11 | contract.py: bafybeiarfpumhdmxh24xuiv6jprjq5f6qlrfrlxisz5enbvvbhotafp37e
12 | fingerprint_ignore_patterns: []
13 | contracts: []
14 | class_name: UniswapV2ERC20Contract
15 | contract_interface_paths:
16 | ethereum: build/IUniswapV2ERC20.json
17 | dependencies: {}
18 |
--------------------------------------------------------------------------------
/operate/http/__init__.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 | """Local resource as an HTTP object."""
21 |
22 | import json
23 | import traceback
24 | import typing as t
25 | from abc import ABC
26 |
27 | from starlette.requests import Request
28 | from starlette.responses import JSONResponse
29 | from starlette.types import Receive, Scope, Send
30 |
31 | from operate.http.exceptions import NotAllowed, ResourceException
32 |
33 |
34 | # pylint: disable=no-self-use
35 |
36 | GenericResource = t.TypeVar("GenericResource")
37 | PostPayload = t.TypeVar("PostPayload")
38 | PostResponse = t.TypeVar("PostResponse")
39 | PutPayload = t.TypeVar("PutPayload")
40 | PutResponse = t.TypeVar("PutResponse")
41 | DeletePayload = t.TypeVar("DeletePayload")
42 | DeleteResponse = t.TypeVar("DeleteResponse")
43 |
44 |
45 | class Resource(
46 | t.Generic[
47 | GenericResource,
48 | PostPayload,
49 | PostResponse,
50 | PutPayload,
51 | PutResponse,
52 | DeletePayload,
53 | DeleteResponse,
54 | ],
55 | ABC,
56 | ):
57 | """Web<->Local resource object."""
58 |
59 | _handlers: t.Dict[str, t.Callable]
60 |
61 | def __init__(self) -> None:
62 | """Initialize object."""
63 | self._handlers = {
64 | "GET": self._get,
65 | "POST": self._post,
66 | "PUT": self._put,
67 | "DELETE": self._delete,
68 | }
69 |
70 | async def access(
71 | self,
72 | params: t.Dict,
73 | scope: Scope,
74 | receive: Receive,
75 | send: Send,
76 | ) -> None:
77 | """Access resource with identifier."""
78 | raise ValueError("No resource identifer defined")
79 |
80 | @property
81 | def json(self) -> GenericResource:
82 | """Return JSON representation of the resource."""
83 | raise NotAllowed("Resource access not allowed")
84 |
85 | def create(self, data: PostPayload) -> PostResponse:
86 | """Create a new resource"""
87 | raise NotAllowed("Resource creation not allowed")
88 |
89 | def update(self, data: PutPayload) -> PutResponse:
90 | """Create a new resource"""
91 | raise NotAllowed("Resource update not allowed")
92 |
93 | def delete(self, data: DeletePayload) -> DeleteResponse:
94 | """Create a new resource"""
95 | raise NotAllowed("Resource deletion not allowed")
96 |
97 | def _get(self) -> GenericResource:
98 | """GET method for the resource."""
99 | return self.json
100 |
101 | def _post(self, data: PostPayload) -> PostResponse:
102 | """POST method for the resource."""
103 | return self.create(data=data)
104 |
105 | def _put(self, data: PutPayload) -> PutResponse:
106 | """PUT method for the resource."""
107 | return self.update(data=data)
108 |
109 | def _delete(self, data: DeletePayload) -> DeleteResponse:
110 | """DELETE method for the resource."""
111 | return self.delete(data=data)
112 |
113 | async def __call__(self, scope: Scope, receive: Receive, send: Send) -> t.Any:
114 | """Web handler for sources."""
115 | request = Request(scope=scope, receive=receive, send=send)
116 | if request.path_params:
117 | await self.access(
118 | scope["path_params"],
119 | scope=scope,
120 | receive=receive,
121 | send=send,
122 | )
123 | return
124 |
125 | try:
126 | handler = self._handlers[request.method]
127 | try:
128 | data = await request.json()
129 | except json.decoder.JSONDecodeError:
130 | data = {}
131 | if request.method == "GET":
132 | content = handler()
133 | else:
134 | content = handler(data)
135 | response = JSONResponse(content=content)
136 | except ResourceException as e:
137 | response = JSONResponse(
138 | content={"error": e.args[0]},
139 | status_code=e.code,
140 | )
141 | except Exception as e: # pylint: disable=broad-except
142 | tb = traceback.format_exc()
143 | response = JSONResponse(
144 | content={"error": str(e), "traceback": tb},
145 | status_code=500,
146 | )
147 | print(tb)
148 | await response(scope=scope, receive=receive, send=send)
149 |
--------------------------------------------------------------------------------
/operate/http/exceptions.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 | """Exceptions."""
21 |
22 |
23 | class ResourceException(Exception):
24 | """Base resource exceptio."""
25 |
26 | code: int
27 |
28 |
29 | class BadRequest(ResourceException):
30 | """Bad request error."""
31 |
32 | code = 400
33 |
34 |
35 | class ResourceAlreadyExists(ResourceException):
36 | """Bad request error."""
37 |
38 | code = 409
39 |
40 |
41 | class NotFound(ResourceException):
42 | """Not found error."""
43 |
44 | code = 404
45 |
46 |
47 | class NotAllowed(ResourceException):
48 | """Not allowed error."""
49 |
50 | code = 405
51 |
--------------------------------------------------------------------------------
/operate/keys.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 | """Keys manager."""
21 |
22 | import json
23 | import logging
24 | import os
25 | import typing as t
26 | from dataclasses import dataclass
27 | from pathlib import Path
28 |
29 | from aea.helpers.logging import setup_logger
30 | from aea_ledger_ethereum.ethereum import EthereumCrypto
31 |
32 | from operate.resource import LocalResource
33 | from operate.types import LedgerType
34 |
35 |
36 | @dataclass
37 | class Key(LocalResource):
38 | """Key resource."""
39 |
40 | ledger: LedgerType
41 | address: str
42 | private_key: str
43 |
44 | @classmethod
45 | def load(cls, path: Path) -> "Key":
46 | """Load a service"""
47 | return super().load(path) # type: ignore
48 |
49 |
50 | Keys = t.List[Key]
51 |
52 |
53 | class KeysManager:
54 | """Keys manager."""
55 |
56 | def __init__(
57 | self,
58 | path: Path,
59 | logger: t.Optional[logging.Logger] = None,
60 | ) -> None:
61 | """
62 | Initialize keys manager
63 |
64 | :param path: Path to keys storage.
65 | :param logger: logging.Logger object.
66 | """
67 | self.path = path
68 | self.logger = logger or setup_logger(name="operate.keys")
69 |
70 | def setup(self) -> None:
71 | """Setup service manager."""
72 | self.path.mkdir(exist_ok=True)
73 |
74 | def get(self, key: str) -> Key:
75 | """Get key object."""
76 | return Key.from_json( # type: ignore
77 | obj=json.loads(
78 | (self.path / key).read_text(
79 | encoding="utf-8",
80 | )
81 | )
82 | )
83 |
84 | def create(self) -> str:
85 | """Creates new key."""
86 | crypto = EthereumCrypto()
87 | path = self.path / crypto.address
88 | if path.is_file():
89 | return crypto.address
90 |
91 | path.write_text(
92 | json.dumps(
93 | Key(
94 | ledger=LedgerType.ETHEREUM,
95 | address=crypto.address,
96 | private_key=crypto.private_key,
97 | ).json,
98 | indent=4,
99 | ),
100 | encoding="utf-8",
101 | )
102 | return crypto.address
103 |
104 | def delete(self, key: str) -> None:
105 | """Delete key."""
106 | os.remove(self.path / key)
107 |
--------------------------------------------------------------------------------
/operate/ledger/__init__.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 | """Ledger helpers."""
21 |
22 | import os
23 | import typing as t
24 |
25 | from operate.ledger.base import LedgerHelper
26 | from operate.ledger.ethereum import Ethereum
27 | from operate.ledger.solana import Solana
28 | from operate.types import ChainType, LedgerType
29 |
30 |
31 | ETHEREUM_PUBLIC_RPC = os.environ.get("DEV_RPC", "https://ethereum.publicnode.com")
32 | GNOSIS_PUBLIC_RPC = os.environ.get("DEV_RPC", "https://gnosis-rpc.publicnode.com")
33 | GOERLI_PUBLIC_RPC = os.environ.get("DEV_RPC", "https://ethereum-goerli.publicnode.com")
34 | SOLANA_PUBLIC_RPC = os.environ.get("DEV_RPC", "https://api.mainnet-beta.solana.com")
35 |
36 | ETHEREUM_RPC = os.environ.get("ETHEREUM_RPC", "https://ethereum.publicnode.com")
37 | GNOSIS_RPC = os.environ.get("DEV_RPC", "https://rpc-gate.autonolas.tech/gnosis-rpc/")
38 | GOERLI_RPC = os.environ.get("DEV_RPC", "https://ethereum-goerli.publicnode.com")
39 | SOLANA_RPC = os.environ.get("DEV_RPC", "https://api.mainnet-beta.solana.com")
40 | OPTIMISM_RPC = os.environ.get("OPTIMISM_RPC", "https://optimism-rpc.publicnode.com")
41 | BASE_RPC = os.environ.get("BASE_RPC", "https://base-rpc.publicnode.com")
42 | MODE_RPC = os.environ.get("MODE_RPC", "https://mainnet.mode.network/")
43 |
44 | PUBLIC_RPCS = {
45 | ChainType.ETHEREUM: ETHEREUM_PUBLIC_RPC,
46 | ChainType.GNOSIS: GNOSIS_PUBLIC_RPC,
47 | ChainType.GOERLI: GOERLI_PUBLIC_RPC,
48 | ChainType.SOLANA: SOLANA_PUBLIC_RPC,
49 | }
50 |
51 | DEFAULT_RPCS = {
52 | ChainType.ETHEREUM: ETHEREUM_RPC,
53 | ChainType.GNOSIS: GNOSIS_RPC,
54 | ChainType.GOERLI: GOERLI_RPC,
55 | ChainType.SOLANA: SOLANA_RPC,
56 | ChainType.OPTIMISM: OPTIMISM_RPC,
57 | ChainType.BASE: BASE_RPC,
58 | ChainType.MODE: MODE_RPC,
59 | }
60 |
61 | CHAIN_HELPERS: t.Dict[ChainType, t.Type[LedgerHelper]] = {
62 | ChainType.ETHEREUM: Ethereum,
63 | ChainType.GNOSIS: Ethereum,
64 | ChainType.GOERLI: Ethereum,
65 | ChainType.BASE: Ethereum,
66 | ChainType.OPTIMISM: Ethereum,
67 | ChainType.SOLANA: Solana,
68 | ChainType.MODE: Ethereum,
69 | }
70 |
71 | LEDGER_HELPERS: t.Dict[LedgerType, t.Type[LedgerHelper]] = {
72 | LedgerType.ETHEREUM: Ethereum,
73 | LedgerType.SOLANA: Solana,
74 | }
75 |
76 | CURRENCY_DENOMS = {
77 | ChainType.ETHEREUM: "Wei",
78 | ChainType.GNOSIS: "xDai",
79 | ChainType.GOERLI: "GWei",
80 | ChainType.SOLANA: "Lamp",
81 | }
82 |
83 |
84 | def get_default_rpc(chain: ChainType) -> str:
85 | """Get default RPC chain type."""
86 | return DEFAULT_RPCS.get(chain, ETHEREUM_RPC)
87 |
88 |
89 | def get_ledger_type_from_chain_type(chain: ChainType) -> LedgerType:
90 | """Get LedgerType from ChainType."""
91 | if chain in (ChainType.ETHEREUM, ChainType.GOERLI, ChainType.GNOSIS):
92 | return LedgerType.ETHEREUM
93 | return LedgerType.SOLANA
94 |
95 |
96 | def get_ledger_helper_by_chain(rpc: str, chain: ChainType) -> LedgerHelper:
97 | """Get ledger helper by chain type."""
98 | return CHAIN_HELPERS.get(chain, Ethereum)(rpc=rpc)
99 |
100 |
101 | def get_ledger_helper_by_ledger(rpc: str, ledger: LedgerHelper) -> LedgerHelper:
102 | """Get ledger helper by ledger type."""
103 | return LEDGER_HELPERS.get(ledger, Ethereum)(rpc=rpc) # type: ignore
104 |
105 |
106 | def get_currency_denom(chain: ChainType) -> str:
107 | """Get currency denom by chain type."""
108 | return CURRENCY_DENOMS.get(chain, "Wei")
109 |
--------------------------------------------------------------------------------
/operate/ledger/base.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 | """Base class."""
21 |
22 | import typing as t
23 | from abc import ABC, abstractmethod
24 |
25 |
26 | class LedgerHelper(ABC): # pylint: disable=too-few-public-methods
27 | """Base ledger helper."""
28 |
29 | api: t.Any
30 |
31 | def __init__(self, rpc: str) -> None:
32 | """Initialize object."""
33 | self.rpc = rpc
34 |
35 | @abstractmethod
36 | def create_key(self) -> t.Any:
37 | """Create key."""
38 |
--------------------------------------------------------------------------------
/operate/ledger/ethereum.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 | """Ethereum ledger helpers."""
21 |
22 | import typing as t
23 |
24 | from aea_ledger_ethereum import EthereumApi, EthereumCrypto
25 |
26 | from operate.ledger.base import LedgerHelper
27 | from operate.types import LedgerType
28 |
29 |
30 | class Ethereum(LedgerHelper):
31 | """Ethereum ledger helper."""
32 |
33 | api: EthereumApi
34 |
35 | def __init__(self, rpc: str) -> None:
36 | """Initialize object."""
37 | super().__init__(rpc)
38 | self.api = EthereumApi(address=self.rpc)
39 |
40 | def create_key(self) -> t.Dict:
41 | """Create key."""
42 | account = EthereumCrypto()
43 | return {
44 | "address": account.address,
45 | "private_key": account.private_key,
46 | "encrypted": False,
47 | "ledger": LedgerType.ETHEREUM,
48 | }
49 |
--------------------------------------------------------------------------------
/operate/ledger/profiles.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 | """Chain profiles."""
21 |
22 | from operate.types import ChainType, ContractAddresses
23 |
24 |
25 | CONTRACTS = {
26 | ChainType.GNOSIS: ContractAddresses(
27 | {
28 | "service_manager": "0x04b0007b2aFb398015B76e5f22993a1fddF83644",
29 | "service_registry": "0x9338b5153AE39BB89f50468E608eD9d764B755fD",
30 | "service_registry_token_utility": "0xa45E64d13A30a51b91ae0eb182e88a40e9b18eD8",
31 | "gnosis_safe_proxy_factory": "0x3C1fF68f5aa342D296d4DEe4Bb1cACCA912D95fE",
32 | "gnosis_safe_same_address_multisig": "0x6e7f594f680f7aBad18b7a63de50F0FeE47dfD06",
33 | "multisend": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D",
34 | }
35 | ),
36 | ChainType.OPTIMISM: ContractAddresses(
37 | {
38 | "service_manager": "0xFbBEc0C8b13B38a9aC0499694A69a10204c5E2aB",
39 | "service_registry": "0x3d77596beb0f130a4415df3D2D8232B3d3D31e44",
40 | "service_registry_token_utility": "0xBb7e1D6Cb6F243D6bdE81CE92a9f2aFF7Fbe7eac",
41 | "gnosis_safe_proxy_factory": "0x5953f21495BD9aF1D78e87bb42AcCAA55C1e896C",
42 | "gnosis_safe_same_address_multisig": "0xb09CcF0Dbf0C178806Aaee28956c74bd66d21f73",
43 | "multisend": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D",
44 | }
45 | ),
46 | ChainType.ETHEREUM: ContractAddresses(
47 | {
48 | "service_manager": "0x2EA682121f815FBcF86EA3F3CaFdd5d67F2dB143",
49 | "service_registry": "0x48b6af7B12C71f09e2fC8aF4855De4Ff54e775cA",
50 | "service_registry_token_utility": "0x3Fb926116D454b95c669B6Bf2E7c3bad8d19affA",
51 | "gnosis_safe_proxy_factory": "0x46C0D07F55d4F9B5Eed2Fc9680B5953e5fd7b461",
52 | "gnosis_safe_same_address_multisig": "0xfa517d01DaA100cB1932FA4345F68874f7E7eF46",
53 | "multisend": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D",
54 | }
55 | ),
56 | ChainType.BASE: ContractAddresses(
57 | {
58 | "service_manager": "0x63e66d7ad413C01A7b49C7FF4e3Bb765C4E4bd1b",
59 | "service_registry": "0x3C1fF68f5aa342D296d4DEe4Bb1cACCA912D95fE",
60 | "service_registry_token_utility": "0x34C895f302D0b5cf52ec0Edd3945321EB0f83dd5",
61 | "gnosis_safe_proxy_factory": "0x22bE6fDcd3e29851B29b512F714C328A00A96B83",
62 | "gnosis_safe_same_address_multisig": "0xFbBEc0C8b13B38a9aC0499694A69a10204c5E2aB",
63 | "multisend": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D",
64 | }
65 | ),
66 | ChainType.MODE: ContractAddresses(
67 | {
68 | "service_manager": "0x63e66d7ad413C01A7b49C7FF4e3Bb765C4E4bd1b",
69 | "service_registry": "0x3C1fF68f5aa342D296d4DEe4Bb1cACCA912D95fE",
70 | "service_registry_token_utility": "0x34C895f302D0b5cf52ec0Edd3945321EB0f83dd5",
71 | "gnosis_safe_proxy_factory": "0xBb7e1D6Cb6F243D6bdE81CE92a9f2aFF7Fbe7eac",
72 | "gnosis_safe_same_address_multisig": "0xFbBEc0C8b13B38a9aC0499694A69a10204c5E2aB",
73 | "multisend": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D",
74 | }
75 | ),
76 | }
77 |
78 | STAKING = {
79 | ChainType.GNOSIS: {
80 | "pearl_alpha": "0xEE9F19b5DF06c7E8Bfc7B28745dcf944C504198A",
81 | "pearl_beta": "0xeF44Fb0842DDeF59D37f85D61A1eF492bbA6135d",
82 | },
83 | ChainType.OPTIMISM: {
84 | "optimus_alpha": "0x88996bbdE7f982D93214881756840cE2c77C4992",
85 | },
86 | ChainType.ETHEREUM: {},
87 | ChainType.BASE: {},
88 | ChainType.MODE: {
89 | "optimus_alpha": "0x5fc25f50E96857373C64dC0eDb1AbCBEd4587e91",
90 | },
91 | }
92 |
93 | OLAS = {
94 | ChainType.GNOSIS: "0xcE11e14225575945b8E6Dc0D4F2dD4C570f79d9f",
95 | ChainType.OPTIMISM: "0xFC2E6e6BCbd49ccf3A5f029c79984372DcBFE527",
96 | ChainType.BASE: "0x54330d28ca3357F294334BDC454a032e7f353416",
97 | ChainType.ETHEREUM: "0x0001A500A6B18995B03f44bb040A5fFc28E45CB0",
98 | ChainType.MODE: "0xcfD1D50ce23C46D3Cf6407487B2F8934e96DC8f9",
99 | }
100 |
--------------------------------------------------------------------------------
/operate/ledger/solana.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 | """Solana ledger helpers."""
21 |
22 | import typing as t
23 |
24 | from operate.ledger.base import LedgerHelper
25 | from operate.types import LedgerType
26 |
27 |
28 | class Solana(LedgerHelper):
29 | """Solana ledger helper."""
30 |
31 | def create_key(self) -> t.Dict:
32 | """Create key."""
33 | return {
34 | "address": "",
35 | "private_key": "",
36 | "encrypted": False,
37 | "ledger": LedgerType.SOLANA,
38 | }
39 |
--------------------------------------------------------------------------------
/operate/pearl.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # ------------------------------------------------------------------------------
3 | #
4 | # Copyright 2024 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 | """File used for pyinstaller to create a single executable file."""
21 | # pylint: disable=all
22 | # mypy: ignore-errors
23 | # flake8: noqa
24 |
25 | import os
26 | import sys
27 | from pathlib import Path
28 |
29 | import aea.configurations.validation as validation_module
30 |
31 |
32 | # patch for the _CUR_DIR value
33 | validation_module._CUR_DIR = Path(sys._MEIPASS) / validation_module._CUR_DIR
34 | validation_module._SCHEMAS_DIR = os.path.join(validation_module._CUR_DIR, "schemas")
35 |
36 |
37 | from aea.crypto.registries.base import *
38 | from aea.mail.base_pb2 import DESCRIPTOR
39 | from aea_ledger_cosmos.cosmos import * # noqa
40 | from aea_ledger_ethereum.ethereum import *
41 | from aea_ledger_ethereum_flashbots.ethereum_flashbots import * # noqa
42 | from google.protobuf.descriptor_pb2 import FileDescriptorProto
43 | from multiaddr.codecs.idna import to_bytes as _
44 | from multiaddr.codecs.uint16be import to_bytes as _
45 |
46 | from operate.cli import main
47 |
48 |
49 | if __name__ == "__main__":
50 | main()
51 |
--------------------------------------------------------------------------------
/operate/resource.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # ------------------------------------------------------------------------------
3 | #
4 | # Copyright 2024 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 | """Local resource representation."""
21 |
22 | import enum
23 | import json
24 | import typing as t
25 | from dataclasses import asdict, is_dataclass
26 | from pathlib import Path
27 |
28 |
29 | # pylint: disable=too-many-return-statements,no-member
30 |
31 |
32 | def serialize(obj: t.Any) -> t.Any:
33 | """Serialize object."""
34 | if is_dataclass(obj):
35 | return asdict(obj)
36 | if isinstance(obj, Path):
37 | return str(obj)
38 | if isinstance(obj, dict):
39 | return {key: serialize(obj=value) for key, value in obj.items()}
40 | if isinstance(obj, list):
41 | return [serialize(obj=value) for value in obj]
42 | if isinstance(obj, enum.Enum):
43 | return obj.value
44 | return obj
45 |
46 |
47 | def deserialize(obj: t.Any, otype: t.Any) -> t.Any:
48 | """Desrialize a json object."""
49 | base = getattr(otype, "__class__") # noqa: B009
50 | if base.__name__ == "_GenericAlias": # type: ignore
51 | args = otype.__args__ # type: ignore
52 | if len(args) == 1:
53 | (atype,) = args
54 | return [deserialize(arg, atype) for arg in obj]
55 | if len(args) == 2:
56 | (ktype, vtype) = args
57 | return {
58 | deserialize(key, ktype): deserialize(val, vtype)
59 | for key, val in obj.items()
60 | }
61 | return obj
62 | if base is enum.EnumMeta:
63 | return otype(obj)
64 | if otype is Path:
65 | return Path(obj)
66 | if is_dataclass(otype):
67 | return otype.from_json(obj)
68 | return obj
69 |
70 |
71 | class LocalResource:
72 | """Initialize local resource."""
73 |
74 | _file: t.Optional[str] = None
75 |
76 | def __init__(self, path: t.Optional[Path] = None) -> None:
77 | """Initialize local resource."""
78 | self.path = path
79 |
80 | @property
81 | def json(self) -> t.Dict:
82 | """To dictionary object."""
83 | obj = {}
84 | for pname, _ in self.__annotations__.items():
85 | if pname.startswith("_") or pname == "path":
86 | continue
87 | obj[pname] = serialize(self.__dict__[pname])
88 | return obj
89 |
90 | @classmethod
91 | def from_json(cls, obj: t.Dict) -> "LocalResource":
92 | """Load LocalResource from json."""
93 | kwargs = {}
94 | for pname, ptype in cls.__annotations__.items():
95 | if pname.startswith("_"):
96 | continue
97 | kwargs[pname] = deserialize(obj=obj[pname], otype=ptype)
98 | return cls(**kwargs)
99 |
100 | @classmethod
101 | def load(cls, path: Path) -> "LocalResource":
102 | """Load local resource."""
103 | file = (
104 | path / cls._file
105 | if cls._file is not None and path.name != cls._file
106 | else path
107 | )
108 | data = json.loads(file.read_text(encoding="utf-8"))
109 | return cls.from_json(obj={**data, "path": path})
110 |
111 | def store(self) -> None:
112 | """Store local resource."""
113 | if self.path is None:
114 | raise RuntimeError(f"Cannot save {self}; Path value not provided.")
115 |
116 | path = self.path
117 | if self._file is not None:
118 | path = path / self._file
119 |
120 | path.write_text(
121 | json.dumps(
122 | self.json,
123 | indent=2,
124 | ),
125 | encoding="utf-8",
126 | )
127 |
--------------------------------------------------------------------------------
/operate/services/__init__.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 | """Services endpoint."""
21 |
22 | from . import manage
23 |
24 |
25 | __all__ = ("manage",)
26 |
--------------------------------------------------------------------------------
/operate/services/health_checker.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 | """Source code for checking aea is alive.."""
21 | import asyncio
22 | import typing as t
23 | from concurrent.futures import ThreadPoolExecutor
24 |
25 | import aiohttp # type: ignore
26 | from aea.helpers.logging import setup_logger
27 |
28 | from operate.services.manage import ServiceManager # type: ignore
29 |
30 |
31 | HTTP_OK = 200
32 |
33 |
34 | class HealthChecker:
35 | """Health checker manager."""
36 |
37 | SLEEP_PERIOD = 30
38 | PORT_UP_TIMEOUT = 120 # seconds
39 |
40 | def __init__(self, service_manager: ServiceManager) -> None:
41 | """Init the healtch checker."""
42 | self._jobs: t.Dict[str, asyncio.Task] = {}
43 | self.logger = setup_logger(name="operate.health_checker")
44 | self.logger.info("[HEALTCHECKER]: created")
45 | self._service_manager = service_manager
46 |
47 | def start_for_service(self, service: str) -> None:
48 | """Start for a specific service."""
49 | self.logger.info(f"[HEALTCHECKER]: Starting healthcheck job for {service}")
50 | if service in self._jobs:
51 | self.stop_for_service(service=service)
52 |
53 | loop = asyncio.get_running_loop()
54 | self._jobs[service] = loop.create_task(
55 | self.healthcheck_job(
56 | service=service,
57 | )
58 | )
59 |
60 | def stop_for_service(self, service: str) -> None:
61 | """Stop for a specific service."""
62 | if service not in self._jobs:
63 | return
64 | self.logger.info(
65 | f"[HEALTCHECKER]: Cancelling existing healthcheck_jobs job for {service}"
66 | )
67 | status = self._jobs[service].cancel()
68 | if not status:
69 | self.logger.info(
70 | f"[HEALTCHECKER]: Healthcheck job cancellation for {service} failed"
71 | )
72 |
73 | @staticmethod
74 | async def check_service_health(service: str) -> bool:
75 | """Check the service health"""
76 | del service
77 | async with aiohttp.ClientSession() as session:
78 | async with session.get("http://localhost:8716/healthcheck") as resp:
79 | status = resp.status
80 | response_json = await resp.json()
81 | return status == HTTP_OK and response_json.get(
82 | "is_transitioning_fast", False
83 | )
84 |
85 | async def healthcheck_job(
86 | self,
87 | service: str,
88 | ) -> None:
89 | """Start a background health check job."""
90 |
91 | try:
92 | self.logger.info(
93 | f"[HEALTCHECKER] Start healthcheck job for service: {service}"
94 | )
95 |
96 | async def _wait_for_port(sleep_period: int = 15) -> None:
97 | self.logger.info("[HEALTCHECKER]: wait port is up")
98 | while True:
99 | try:
100 | await self.check_service_health(service)
101 | self.logger.info("[HEALTCHECKER]: port is UP")
102 | return
103 | except aiohttp.ClientConnectionError:
104 | self.logger.error("[HEALTCHECKER]: error connecting http port")
105 | await asyncio.sleep(sleep_period)
106 |
107 | async def _check_port_ready(
108 | timeout: int = self.PORT_UP_TIMEOUT, sleep_period: int = 15
109 | ) -> bool:
110 | try:
111 | await asyncio.wait_for(
112 | _wait_for_port(sleep_period=sleep_period), timeout=timeout
113 | )
114 | return True
115 | except asyncio.TimeoutError:
116 | return False
117 |
118 | async def _check_health(
119 | number_of_fails: int = 5, sleep_period: int = self.SLEEP_PERIOD
120 | ) -> None:
121 | fails = 0
122 | while True:
123 | try:
124 | # Check the service health
125 | healthy = await self.check_service_health(service)
126 | except aiohttp.ClientConnectionError:
127 | self.logger.info(
128 | f"[HEALTCHECKER] {service} port read failed. restart"
129 | )
130 | return
131 |
132 | if not healthy:
133 | fails += 1
134 | self.logger.info(
135 | f"[HEALTCHECKER] {service} not healthy for {fails} time in a row"
136 | )
137 | else:
138 | self.logger.info(f"[HEALTCHECKER] {service} is HEALTHY")
139 | # reset fails if comes healty
140 | fails = 0
141 |
142 | if fails >= number_of_fails:
143 | # too much fails, exit
144 | self.logger.error(
145 | f"[HEALTCHECKER] {service} failed {fails} times in a row. restart"
146 | )
147 | return
148 | await asyncio.sleep(sleep_period)
149 |
150 | async def _restart(service_manager: ServiceManager, service: str) -> None:
151 | def _do_restart() -> None:
152 | service_manager.stop_service_locally(hash=service)
153 | service_manager.deploy_service_locally(hash=service)
154 |
155 | loop = asyncio.get_event_loop()
156 | with ThreadPoolExecutor() as executor:
157 | future = loop.run_in_executor(executor, _do_restart)
158 | await future
159 | exception = future.exception()
160 | if exception is not None:
161 | raise exception
162 |
163 | # upper cycle
164 | while True:
165 | self.logger.info(f"[HEALTCHECKER] {service} wait for port ready")
166 | if await _check_port_ready(timeout=self.PORT_UP_TIMEOUT):
167 | # blocking till restart needed
168 | self.logger.info(
169 | f"[HEALTCHECKER] {service} port is ready, checking health every {self.SLEEP_PERIOD}"
170 | )
171 | await _check_health(sleep_period=self.SLEEP_PERIOD)
172 |
173 | else:
174 | self.logger.info(
175 | "[HEALTCHECKER] port not ready within timeout. restart deployment"
176 | )
177 |
178 | # perform restart
179 | # TODO: blocking!!!!!!!
180 | await _restart(self._service_manager, service)
181 | except Exception:
182 | self.logger.exception(f"problems running healthcheckr for {service}")
183 | raise
184 |
--------------------------------------------------------------------------------
/operate/services/utils/__init__.py:
--------------------------------------------------------------------------------
1 | """Deployment utils."""
2 |
--------------------------------------------------------------------------------
/operate/types.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 | """Types module."""
21 |
22 | import enum
23 | import typing as t
24 | from dataclasses import dataclass
25 |
26 | from typing_extensions import TypedDict
27 |
28 | from operate.resource import LocalResource
29 |
30 |
31 | _ACTIONS = {
32 | "status": 0,
33 | "build": 1,
34 | "deploy": 2,
35 | "stop": 3,
36 | }
37 |
38 |
39 | _CHAIN_NAME_TO_ENUM = {
40 | "ethereum": 0,
41 | "goerli": 1,
42 | "gnosis": 2,
43 | "solana": 3,
44 | "optimism": 4,
45 | "base": 5,
46 | "mode": 6,
47 | }
48 |
49 | _CHAIN_ID_TO_CHAIN_NAME = {
50 | 1: "ethereum",
51 | 5: "goerli",
52 | 100: "gnosis",
53 | 1399811149: "solana",
54 | 10: "optimism",
55 | 8453: "base",
56 | 34443: "mode",
57 | }
58 |
59 | _CHAIN_NAME_TO_ID = {val: key for key, val in _CHAIN_ID_TO_CHAIN_NAME.items()}
60 |
61 | _LEDGER_TYPE_TO_ENUM = {
62 | "ethereum": 0,
63 | "solana": 1,
64 | }
65 |
66 |
67 | class LedgerType(enum.IntEnum):
68 | """Ledger type enum."""
69 |
70 | ETHEREUM = 0
71 | SOLANA = 1
72 |
73 | @classmethod
74 | def from_string(cls, chain: str) -> "LedgerType":
75 | """Load from string."""
76 | return cls(_LEDGER_TYPE_TO_ENUM[chain.lower()])
77 |
78 | @property
79 | def config_file(self) -> str:
80 | """Config filename."""
81 | return f"{self.name.lower()}.json"
82 |
83 | @property
84 | def key_file(self) -> str:
85 | """Key filename."""
86 | return f"{self.name.lower()}.txt"
87 |
88 |
89 | class ChainType(enum.IntEnum):
90 | """Chain type enum."""
91 |
92 | ETHEREUM = 0
93 | GOERLI = 1
94 | GNOSIS = 2
95 | SOLANA = 3
96 | OPTIMISM = 4
97 | BASE = 5
98 | MODE = 6
99 |
100 | @property
101 | def id(self) -> int:
102 | """Returns chain id."""
103 | return _CHAIN_NAME_TO_ID[self.name.lower()]
104 |
105 | @classmethod
106 | def from_string(cls, chain: str) -> "ChainType":
107 | """Load from string."""
108 | return cls(_CHAIN_NAME_TO_ENUM[chain.lower()])
109 |
110 | @classmethod
111 | def from_id(cls, cid: int) -> "ChainType":
112 | """Load from chain ID."""
113 | return cls(_CHAIN_NAME_TO_ENUM[_CHAIN_ID_TO_CHAIN_NAME[cid]])
114 |
115 |
116 | class Action(enum.IntEnum):
117 | """Action payload."""
118 |
119 | STATUS = 0
120 | BUILD = 1
121 | DEPLOY = 2
122 | STOP = 3
123 |
124 | @classmethod
125 | def from_string(cls, action: str) -> "Action":
126 | """Load from string."""
127 | return cls(_ACTIONS[action])
128 |
129 |
130 | class DeploymentStatus(enum.IntEnum):
131 | """Status payload."""
132 |
133 | CREATED = 0
134 | BUILT = 1
135 | DEPLOYING = 2
136 | DEPLOYED = 3
137 | STOPPING = 4
138 | STOPPED = 5
139 | DELETED = 6
140 |
141 |
142 | # TODO defined in aea.chain.base.OnChainState
143 | class OnChainState(enum.IntEnum):
144 | """On-chain state."""
145 |
146 | NON_EXISTENT = 0
147 | PRE_REGISTRATION = 1
148 | ACTIVE_REGISTRATION = 2
149 | FINISHED_REGISTRATION = 3
150 | DEPLOYED = 4
151 | TERMINATED_BONDED = 5
152 | UNBONDED = 6 # TODO this is not an on-chain state https://github.com/valory-xyz/autonolas-registries/blob/main/contracts/ServiceRegistryL2.sol
153 |
154 |
155 | class ContractAddresses(TypedDict):
156 | """Contracts templates."""
157 |
158 | service_manager: str
159 | service_registry: str
160 | service_registry_token_utility: str
161 | gnosis_safe_proxy_factory: str
162 | gnosis_safe_same_address_multisig: str
163 | multisend: str
164 |
165 |
166 | @dataclass
167 | class LedgerConfig(LocalResource):
168 | """Ledger config."""
169 |
170 | rpc: str
171 | type: LedgerType
172 | chain: ChainType
173 |
174 |
175 | LedgerConfigs = t.Dict[str, LedgerConfig]
176 |
177 |
178 | class DeploymentConfig(TypedDict):
179 | """Deployments template."""
180 |
181 | volumes: t.Dict[str, str]
182 |
183 |
184 | class FundRequirementsTemplate(TypedDict):
185 | """Fund requirement template."""
186 |
187 | agent: int
188 | safe: int
189 |
190 |
191 | class ConfigurationTemplate(TypedDict):
192 | """Configuration template."""
193 |
194 | staking_program_id: str
195 | nft: str
196 | rpc: str
197 | threshold: int
198 | use_staking: bool
199 | cost_of_bond: int
200 | fund_requirements: FundRequirementsTemplate
201 | fallback_chain_params: t.Optional[t.Dict]
202 |
203 |
204 | ConfigurationTemplates = t.Dict[str, ConfigurationTemplate]
205 |
206 |
207 | class ServiceTemplate(TypedDict):
208 | """Service template."""
209 |
210 | name: str
211 | hash: str
212 | image: str
213 | description: str
214 | service_version: str
215 | home_chain_id: str
216 | configurations: ConfigurationTemplates
217 |
218 |
219 | @dataclass
220 | class DeployedNodes(LocalResource):
221 | """Deployed nodes type."""
222 |
223 | agent: t.List[str]
224 | tendermint: t.List[str]
225 |
226 |
227 | @dataclass
228 | class OnChainFundRequirements(LocalResource):
229 | """On-chain fund requirements."""
230 |
231 | agent: float
232 | safe: float
233 |
234 |
235 | @dataclass
236 | class OnChainUserParams(LocalResource):
237 | """On-chain user params."""
238 |
239 | staking_program_id: str
240 | nft: str
241 | threshold: int
242 | use_staking: bool
243 | cost_of_bond: int
244 | fund_requirements: OnChainFundRequirements
245 |
246 | @classmethod
247 | def from_json(cls, obj: t.Dict) -> "OnChainUserParams":
248 | """Load a service"""
249 | return super().from_json(obj) # type: ignore
250 |
251 |
252 | @dataclass
253 | class OnChainData(LocalResource):
254 | """On-chain data"""
255 |
256 | instances: t.List[str] # Agent instances registered as safe owners
257 | token: int
258 | multisig: str
259 | staked: bool
260 | on_chain_state: OnChainState
261 | user_params: OnChainUserParams
262 |
263 |
264 | @dataclass
265 | class ChainConfig(LocalResource):
266 | """Chain config."""
267 |
268 | ledger_config: LedgerConfig
269 | chain_data: OnChainData
270 |
271 | @classmethod
272 | def from_json(cls, obj: t.Dict) -> "ChainConfig":
273 | """Load the chain config."""
274 | return super().from_json(obj) # type: ignore
275 |
276 |
277 | ChainConfigs = t.Dict[str, ChainConfig]
278 |
--------------------------------------------------------------------------------
/operate/utils/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # ------------------------------------------------------------------------------
3 | #
4 | # Copyright 2024 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 | """Helper utilities."""
21 |
--------------------------------------------------------------------------------
/operate/utils/gnosis.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 | """Safe helpers."""
21 |
22 | import binascii
23 | import os
24 | import secrets
25 | import typing as t
26 | from enum import Enum
27 |
28 | from aea.crypto.base import Crypto, LedgerApi
29 | from autonomy.chain.base import registry_contracts
30 | from autonomy.chain.config import ChainType as ChainProfile
31 | from autonomy.chain.tx import TxSettler
32 |
33 | from operate.constants import (
34 | ON_CHAIN_INTERACT_RETRIES,
35 | ON_CHAIN_INTERACT_SLEEP,
36 | ON_CHAIN_INTERACT_TIMEOUT,
37 | )
38 |
39 |
40 | NULL_ADDRESS: str = "0x" + "0" * 40
41 | MAX_UINT256 = 2**256 - 1
42 | ZERO_ETH = 0
43 |
44 |
45 | class SafeOperation(Enum):
46 | """Operation types."""
47 |
48 | CALL = 0
49 | DELEGATE_CALL = 1
50 | CREATE = 2
51 |
52 |
53 | class MultiSendOperation(Enum):
54 | """Operation types."""
55 |
56 | CALL = 0
57 | DELEGATE_CALL = 1
58 |
59 |
60 | def hash_payload_to_hex( # pylint: disable=too-many-arguments,too-many-locals
61 | safe_tx_hash: str,
62 | ether_value: int,
63 | safe_tx_gas: int,
64 | to_address: str,
65 | data: bytes,
66 | operation: int = SafeOperation.CALL.value,
67 | base_gas: int = 0,
68 | safe_gas_price: int = 0,
69 | gas_token: str = NULL_ADDRESS,
70 | refund_receiver: str = NULL_ADDRESS,
71 | use_flashbots: bool = False,
72 | gas_limit: int = 0,
73 | raise_on_failed_simulation: bool = False,
74 | ) -> str:
75 | """Serialise to a hex string."""
76 | if len(safe_tx_hash) != 64: # should be exactly 32 bytes!
77 | raise ValueError(
78 | "cannot encode safe_tx_hash of non-32 bytes"
79 | ) # pragma: nocover
80 |
81 | if len(to_address) != 42 or len(gas_token) != 42 or len(refund_receiver) != 42:
82 | raise ValueError("cannot encode address of non 42 length") # pragma: nocover
83 |
84 | if (
85 | ether_value > MAX_UINT256
86 | or safe_tx_gas > MAX_UINT256
87 | or base_gas > MAX_UINT256
88 | or safe_gas_price > MAX_UINT256
89 | or gas_limit > MAX_UINT256
90 | ):
91 | raise ValueError(
92 | "Value is bigger than the max 256 bit value"
93 | ) # pragma: nocover
94 |
95 | if operation not in [v.value for v in SafeOperation]:
96 | raise ValueError("SafeOperation value is not valid") # pragma: nocover
97 |
98 | if not isinstance(use_flashbots, bool):
99 | raise ValueError(
100 | f"`use_flashbots` value ({use_flashbots}) is not valid. A boolean value was expected instead"
101 | )
102 |
103 | ether_value_ = ether_value.to_bytes(32, "big").hex()
104 | safe_tx_gas_ = safe_tx_gas.to_bytes(32, "big").hex()
105 | operation_ = operation.to_bytes(1, "big").hex()
106 | base_gas_ = base_gas.to_bytes(32, "big").hex()
107 | safe_gas_price_ = safe_gas_price.to_bytes(32, "big").hex()
108 | use_flashbots_ = use_flashbots.to_bytes(32, "big").hex()
109 | gas_limit_ = gas_limit.to_bytes(32, "big").hex()
110 | raise_on_failed_simulation_ = raise_on_failed_simulation.to_bytes(32, "big").hex()
111 |
112 | concatenated = (
113 | safe_tx_hash
114 | + ether_value_
115 | + safe_tx_gas_
116 | + to_address
117 | + operation_
118 | + base_gas_
119 | + safe_gas_price_
120 | + gas_token
121 | + refund_receiver
122 | + use_flashbots_
123 | + gas_limit_
124 | + raise_on_failed_simulation_
125 | + data.hex()
126 | )
127 | return concatenated
128 |
129 |
130 | def skill_input_hex_to_payload(payload: str) -> dict:
131 | """Decode payload."""
132 | tx_params = dict(
133 | safe_tx_hash=payload[:64],
134 | ether_value=int.from_bytes(bytes.fromhex(payload[64:128]), "big"),
135 | safe_tx_gas=int.from_bytes(bytes.fromhex(payload[128:192]), "big"),
136 | to_address=payload[192:234],
137 | operation=int.from_bytes(bytes.fromhex(payload[234:236]), "big"),
138 | base_gas=int.from_bytes(bytes.fromhex(payload[236:300]), "big"),
139 | safe_gas_price=int.from_bytes(bytes.fromhex(payload[300:364]), "big"),
140 | gas_token=payload[364:406],
141 | refund_receiver=payload[406:448],
142 | use_flashbots=bool.from_bytes(bytes.fromhex(payload[448:512]), "big"),
143 | gas_limit=int.from_bytes(bytes.fromhex(payload[512:576]), "big"),
144 | raise_on_failed_simulation=bool.from_bytes(
145 | bytes.fromhex(payload[576:640]), "big"
146 | ),
147 | data=bytes.fromhex(payload[640:]),
148 | )
149 | return tx_params
150 |
151 |
152 | def _get_nonce() -> int:
153 | """Generate a nonce for the Safe deployment."""
154 | return secrets.SystemRandom().randint(0, 2**256 - 1)
155 |
156 |
157 | def create_safe(
158 | ledger_api: LedgerApi,
159 | crypto: Crypto,
160 | owner: t.Optional[str] = None,
161 | salt_nonce: t.Optional[int] = None,
162 | ) -> t.Tuple[str, int]:
163 | """Create gnosis safe."""
164 | salt_nonce = salt_nonce or _get_nonce()
165 |
166 | def _build( # pylint: disable=unused-argument
167 | *args: t.Any, **kwargs: t.Any
168 | ) -> t.Dict:
169 | tx = registry_contracts.gnosis_safe.get_deploy_transaction(
170 | ledger_api=ledger_api,
171 | deployer_address=crypto.address,
172 | owners=[crypto.address] if owner is None else [crypto.address, owner],
173 | threshold=1,
174 | salt_nonce=salt_nonce,
175 | )
176 | del tx["contract_address"]
177 | return tx
178 |
179 | tx_settler = TxSettler(
180 | ledger_api=ledger_api,
181 | crypto=crypto,
182 | chain_type=ChainProfile.CUSTOM,
183 | timeout=ON_CHAIN_INTERACT_TIMEOUT,
184 | retries=ON_CHAIN_INTERACT_RETRIES,
185 | sleep=ON_CHAIN_INTERACT_SLEEP,
186 | )
187 | setattr( # noqa: B010
188 | tx_settler,
189 | "build",
190 | _build,
191 | )
192 | receipt = tx_settler.transact(
193 | method=lambda: {},
194 | contract="",
195 | kwargs={},
196 | )
197 | instance = registry_contracts.gnosis_safe_proxy_factory.get_instance(
198 | ledger_api=ledger_api,
199 | contract_address="0xa6b71e26c5e0845f74c812102ca7114b6a896ab2",
200 | )
201 | (event,) = instance.events.ProxyCreation().process_receipt(receipt)
202 | return event["args"]["proxy"], salt_nonce
203 |
204 |
205 | def get_owners(ledger_api: LedgerApi, safe: str) -> t.List[str]:
206 | """Get list of owners."""
207 | return registry_contracts.gnosis_safe.get_owners(
208 | ledger_api=ledger_api,
209 | contract_address=safe,
210 | ).get("owners", [])
211 |
212 |
213 | def send_safe_txs(
214 | txd: bytes,
215 | safe: str,
216 | ledger_api: LedgerApi,
217 | crypto: Crypto,
218 | to: t.Optional[str] = None,
219 | ) -> None:
220 | """Send internal safe transaction."""
221 | owner = ledger_api.api.to_checksum_address(
222 | crypto.address,
223 | )
224 | to_address = to or safe
225 | safe_tx_hash = registry_contracts.gnosis_safe.get_raw_safe_transaction_hash(
226 | ledger_api=ledger_api,
227 | contract_address=safe,
228 | value=0,
229 | safe_tx_gas=0,
230 | to_address=to_address,
231 | data=txd,
232 | operation=SafeOperation.CALL.value,
233 | ).get("tx_hash")
234 | safe_tx_bytes = binascii.unhexlify(
235 | safe_tx_hash[2:],
236 | )
237 | signatures = {
238 | owner: crypto.sign_message(
239 | message=safe_tx_bytes,
240 | is_deprecated_mode=True,
241 | )[2:]
242 | }
243 | max_priority_fee_per_gas = os.getenv("MAX_PRIORITY_FEE_PER_GAS", None)
244 | max_fee_per_gas = os.getenv("MAX_FEE_PER_GAS", None)
245 | transaction = registry_contracts.gnosis_safe.get_raw_safe_transaction(
246 | ledger_api=ledger_api,
247 | contract_address=safe,
248 | sender_address=owner,
249 | owners=(owner,), # type: ignore
250 | to_address=to_address,
251 | value=0,
252 | data=txd,
253 | safe_tx_gas=0,
254 | signatures_by_owner=signatures,
255 | operation=SafeOperation.CALL.value,
256 | nonce=ledger_api.api.eth.get_transaction_count(owner),
257 | max_fee_per_gas=int(max_fee_per_gas) if max_fee_per_gas else None,
258 | max_priority_fee_per_gas=int(max_priority_fee_per_gas)
259 | if max_priority_fee_per_gas
260 | else None,
261 | )
262 | ledger_api.get_transaction_receipt(
263 | ledger_api.send_signed_transaction(
264 | crypto.sign_transaction(
265 | transaction,
266 | ),
267 | )
268 | )
269 |
270 |
271 | def add_owner(
272 | ledger_api: LedgerApi,
273 | crypto: Crypto,
274 | safe: str,
275 | owner: str,
276 | ) -> None:
277 | """Add owner to a safe."""
278 | instance = registry_contracts.gnosis_safe.get_instance(
279 | ledger_api=ledger_api,
280 | contract_address=safe,
281 | )
282 | txd = instance.encodeABI(
283 | fn_name="addOwnerWithThreshold",
284 | args=[
285 | owner,
286 | 1,
287 | ],
288 | )
289 | send_safe_txs(
290 | txd=bytes.fromhex(txd[2:]),
291 | safe=safe,
292 | ledger_api=ledger_api,
293 | crypto=crypto,
294 | )
295 |
296 |
297 | def swap_owner( # pylint: disable=unused-argument
298 | ledger_api: LedgerApi,
299 | crypto: Crypto,
300 | safe: str,
301 | old_owner: str,
302 | new_owner: str,
303 | ) -> None:
304 | """Swap owner on a safe."""
305 |
306 |
307 | def transfer(
308 | ledger_api: LedgerApi,
309 | crypto: Crypto,
310 | safe: str,
311 | to: str,
312 | amount: t.Union[float, int],
313 | ) -> None:
314 | """Transfer assets from safe to given address."""
315 | amount = int(amount)
316 | owner = ledger_api.api.to_checksum_address(
317 | crypto.address,
318 | )
319 | safe_tx_hash = registry_contracts.gnosis_safe.get_raw_safe_transaction_hash(
320 | ledger_api=ledger_api,
321 | contract_address=safe,
322 | value=amount,
323 | safe_tx_gas=0,
324 | to_address=to,
325 | data=b"",
326 | operation=SafeOperation.CALL.value,
327 | ).get("tx_hash")
328 | safe_tx_bytes = binascii.unhexlify(
329 | safe_tx_hash[2:],
330 | )
331 | signatures = {
332 | owner: crypto.sign_message(
333 | message=safe_tx_bytes,
334 | is_deprecated_mode=True,
335 | )[2:]
336 | }
337 | max_priority_fee_per_gas = os.getenv("MAX_PRIORITY_FEE_PER_GAS", None)
338 | max_fee_per_gas = os.getenv("MAX_FEE_PER_GAS", None)
339 | transaction = registry_contracts.gnosis_safe.get_raw_safe_transaction(
340 | ledger_api=ledger_api,
341 | contract_address=safe,
342 | sender_address=owner,
343 | owners=(owner,), # type: ignore
344 | to_address=to,
345 | value=amount,
346 | data=b"",
347 | safe_tx_gas=0,
348 | signatures_by_owner=signatures,
349 | operation=SafeOperation.CALL.value,
350 | nonce=ledger_api.api.eth.get_transaction_count(owner),
351 | max_fee_per_gas=int(max_fee_per_gas) if max_fee_per_gas else None,
352 | max_priority_fee_per_gas=int(max_priority_fee_per_gas)
353 | if max_priority_fee_per_gas
354 | else None,
355 | )
356 | ledger_api.get_transaction_receipt(
357 | ledger_api.send_signed_transaction(
358 | crypto.sign_transaction(
359 | transaction,
360 | ),
361 | )
362 | )
363 |
364 |
365 | def transfer_erc20_from_safe(
366 | ledger_api: LedgerApi,
367 | crypto: Crypto,
368 | safe: str,
369 | token: str,
370 | to: str,
371 | amount: t.Union[float, int],
372 | ) -> None:
373 | """Transfer ERC20 assets from safe to given address."""
374 | amount = int(amount)
375 | instance = registry_contracts.erc20.get_instance(
376 | ledger_api=ledger_api,
377 | contract_address=token,
378 | )
379 | txd = instance.encodeABI(
380 | fn_name="transfer",
381 | args=[
382 | to,
383 | amount,
384 | ],
385 | )
386 | send_safe_txs(
387 | txd=bytes.fromhex(txd[2:]),
388 | safe=safe,
389 | ledger_api=ledger_api,
390 | crypto=crypto,
391 | to=token,
392 | )
393 |
--------------------------------------------------------------------------------
/operate/wallet/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # ------------------------------------------------------------------------------
3 | #
4 | # Copyright 2024 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 | """Wallet implementation."""
21 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "author": "Valory AG",
3 | "dependencies": {
4 | "@ant-design/cssinjs": "^1.18.4",
5 | "@ant-design/icons": "^5.3.0",
6 | "@fontsource/inter": "^5.0.17",
7 | "adm-zip": "^0.5.12",
8 | "antd": "^5.14.0",
9 | "axios": "^1.7.2",
10 | "child_process": "^1.0.2",
11 | "cross-env": "^7.0.3",
12 | "dotenv": "^16.4.5",
13 | "electron-log": "^5.1.4",
14 | "electron-store": "8.2.0",
15 | "electron-updater": "^6.1.8",
16 | "ethers": "5.7.2",
17 | "ethers-multicall": "^0.2.3",
18 | "lodash": "^4.17.21",
19 | "next": "^14.2.3",
20 | "ps-tree": "^1.2.0",
21 | "react": "^18.3.1",
22 | "react-dom": "^18.3.1",
23 | "sass": "^1.72.0",
24 | "styled-components": "^6.1.8",
25 | "sudo-prompt": "9.2.1",
26 | "usehooks-ts": "^2.14.0",
27 | "winston": "^3.13.0"
28 | },
29 | "devDependencies": {
30 | "@electron/notarize": "^2.3.0",
31 | "dotenv-cli": "^7.4.2",
32 | "electron": "30.0.6",
33 | "electron-builder": "^24.12.0",
34 | "eslint": "^8.56.0",
35 | "eslint-config-prettier": "^9.1.0",
36 | "eslint-plugin-prettier": "^5.1.3",
37 | "hardhat": "==2.17.1",
38 | "net": "^1.0.2",
39 | "prettier": "^3.2.5"
40 | },
41 | "main": "electron/main.js",
42 | "name": "olas-operate-app",
43 | "productName": "Pearl",
44 | "description": "An all-in-one application designed to streamline your entry into the world of autonomous agents and earning OLAS through staking.",
45 | "scripts": {
46 | "build:frontend": "cd frontend && yarn build && rm -rf ../electron/.next && cp -r .next ../electron/.next && rm -rf ../electron/public && cp -r public ../electron/public",
47 | "dev:backend": "poetry run python operate/cli.py",
48 | "dev:frontend": "cd frontend && yarn dev",
49 | "dev:hardhat": "hardhat node",
50 | "install-deps": "yarn && yarn install:backend && yarn install:frontend",
51 | "install:backend": "poetry install --no-root",
52 | "install:frontend": "cd frontend && yarn",
53 | "lint:frontend": "cd frontend && yarn lint",
54 | "start": "yarn electron .",
55 | "dev": "dotenv -e .env -- yarn start",
56 | "start:frontend": "cd frontend && yarn start",
57 | "test:frontend": "cd frontend && yarn test",
58 | "download-binaries": "sh download_binaries.sh",
59 | "build:pearl": "sh build_pearl.sh"
60 | },
61 | "version": "0.1.0-rc115"
62 | }
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.poetry]
2 | name = "olas-operate-middleware"
3 | version = "0.1.0-rc115"
4 | description = ""
5 | authors = ["David Vilela ", "Viraj Patel "]
6 | readme = "README.md"
7 | packages = [
8 | { include = "operate" }
9 | ]
10 | include = [
11 | "operate/data/acn/*",
12 | "operate/data/contracts/*",
13 | "operate/data/contracts/service_staking_token/*",
14 | "operate/data/contracts/service_staking_token/build/*",
15 | ]
16 |
17 | [tool.poetry.scripts]
18 | operate = "operate.cli:main"
19 |
20 | [tool.poetry.dependencies]
21 | python = "<3.12,>=3.9"
22 | open-autonomy = "==0.19.4"
23 | open-aea-ledger-cosmos = "==1.64.0"
24 | open-aea-ledger-ethereum = "==1.64.0"
25 | open-aea-ledger-ethereum-flashbots = "==1.64.0"
26 | open-aea-cli-ipfs = "==1.64.0"
27 | clea = "==0.1.0rc4"
28 | cytoolz = "==0.12.3"
29 | docker = "6.1.2"
30 | fastapi = "==0.110.0"
31 | eth-hash = "==0.7.0"
32 | eth-account = "==0.8.0"
33 | eth-keyfile = "==0.6.1"
34 | eth-keys = "==0.4.0"
35 | eth-rlp = "==0.3.0"
36 | eth-typing = "==3.5.2"
37 | eth-utils = "==2.3.1"
38 | eth-abi = "==5.1.0"
39 | frozenlist = "==1.4.1"
40 | hexbytes = "==0.3.1"
41 | ipfshttpclient = "==0.8.0a2"
42 | jsonschema = "==4.3.3"
43 | multidict = "==6.0.5"
44 | requests-toolbelt = "1.0.0"
45 | starlette = "==0.36.3"
46 | uvicorn = "==0.27.0"
47 | web3 = "==6.1.0"
48 | psutil = "^5.9.8"
49 | pyinstaller = "^6.8.0"
50 | aiohttp = "3.9.5"
51 | halo = "^0.0.31"
52 | icecream = "^2.1.3"
53 |
54 | [tool.poetry.group.development.dependencies]
55 | tomte = {version = "0.2.17", extras = ["cli"]}
56 |
57 | [build-system]
58 | requires = ["poetry-core"]
59 | build-backend = "poetry.core.masonry.api"
60 |
--------------------------------------------------------------------------------
/report.py:
--------------------------------------------------------------------------------
1 | # report.py
2 | import warnings
3 | warnings.filterwarnings("ignore", category=UserWarning)
4 | import json
5 | from datetime import datetime
6 | import logging
7 | from decimal import Decimal, getcontext
8 |
9 | from run_service import (
10 | load_local_config,
11 | get_service_template,
12 | CHAIN_ID_TO_METADATA,
13 | OPERATE_HOME
14 | )
15 |
16 | from utils import (
17 | _print_section_header,
18 | _print_subsection_header,
19 | _print_status,
20 | wei_to_eth,
21 | _warning_message,
22 | get_chain_name,
23 | load_operator_address,
24 | validate_config,
25 | _get_agent_status,
26 | )
27 |
28 | from run_service import fetch_agent_fund_requirement
29 | from wallet_info import save_wallet_info, load_config as load_wallet_config
30 | from staking_report import staking_report
31 |
32 | # Set decimal precision
33 | getcontext().prec = 18
34 |
35 | # Configure logging
36 | logging.basicConfig(level=logging.INFO, format='%(message)s')
37 |
38 | def load_wallet_info():
39 | save_wallet_info()
40 | file_path = OPERATE_HOME / "wallets" / "wallet_info.json"
41 | try:
42 | with open(file_path, "r") as f:
43 | return json.load(f)
44 | except FileNotFoundError:
45 | print(f"Error: Wallet info file not found at {file_path}")
46 | return {}
47 | except json.JSONDecodeError:
48 | print("Error: Wallet info file contains invalid JSON.")
49 | return {}
50 |
51 | def get_chain_rpc(optimus_config, chain_name):
52 | chain_name_to_rpc = {
53 | 'mode': optimus_config.mode_rpc
54 | }
55 | return chain_name_to_rpc.get(chain_name.lower())
56 |
57 | def generate_report():
58 | try:
59 | # First, update the wallet info
60 | wallet_info = load_wallet_info()
61 | if not wallet_info:
62 | print("Error: Wallet info is empty.")
63 | return
64 |
65 | operator_address = load_operator_address(OPERATE_HOME)
66 | if not operator_address:
67 | print("Error: Operator address could not be loaded.")
68 | return
69 |
70 | config = load_wallet_config()
71 | if not config:
72 | print("Error: Config is empty.")
73 | return
74 |
75 | if not validate_config(config):
76 | return
77 |
78 | optimus_config = load_local_config()
79 | # Service Report Header
80 | print("")
81 | print("==============")
82 | print("Olas Modius Service Report")
83 | print("Generated on: ", datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
84 | print("==============")
85 |
86 | staking_report(config)
87 | home_chain_id = config.get("home_chain_id")
88 | service_id = config.get("chain_configs", {}).get(str(home_chain_id), {}).get("chain_data", {}).get("token")
89 | if not service_id:
90 | print(f"Error: 'token' not found in chain data for chain ID {home_chain_id}.")
91 | return
92 |
93 | # Service Section
94 | _print_section_header("Service")
95 | _print_status("ID", str(service_id))
96 |
97 | # Agent Section
98 | agent_status = _get_agent_status()
99 | _print_subsection_header("Agent")
100 | _print_status("Status (on this machine)", agent_status)
101 | _print_status("Address", wallet_info.get('main_wallet_address', 'N/A'))
102 |
103 | for chain_id, chain_config in config.get("chain_configs", {}).items():
104 | chain_name = get_chain_name(chain_id, CHAIN_ID_TO_METADATA)
105 | if optimus_config.allowed_chains and chain_name.lower() not in optimus_config.allowed_chains:
106 | continue
107 | balance_info = wallet_info.get('main_wallet_balances', {}).get(chain_name, {})
108 | balance_formatted = balance_info.get('balance_formatted', 'N/A')
109 | _print_status(f"{chain_name} Balance", balance_formatted)
110 |
111 | # Low balance check
112 | agent_threshold_wei = fetch_agent_fund_requirement(chain_id, get_chain_rpc(optimus_config, chain_name))
113 | if agent_threshold_wei is None:
114 | agent_threshold_wei = chain_config.get("chain_data", {}).get("user_params", {}).get("fund_requirements", {}).get("agent")
115 | if agent_threshold_wei:
116 | agent_threshold_eth = wei_to_eth(agent_threshold_wei)
117 | current_balance_str = balance_formatted.split()[0]
118 | try:
119 | current_balance = Decimal(current_balance_str)
120 | if current_balance < agent_threshold_eth:
121 | warning_msg = _warning_message(current_balance, agent_threshold_eth, f"Balance below threshold of {agent_threshold_eth:.5f} ETH")
122 | _print_status("Warning", warning_msg)
123 | except (ValueError, Exception):
124 | print(f"Warning: Could not parse balance '{balance_formatted}' for chain '{chain_name}'.")
125 |
126 | # Safe Section
127 | _print_subsection_header("Safe")
128 | safe_balances = wallet_info.get('safe_balances', {})
129 | for chain_id, chain_config in config.get("chain_configs", {}).items():
130 | chain_name = get_chain_name(chain_id, CHAIN_ID_TO_METADATA)
131 | if optimus_config.allowed_chains and chain_name.lower() not in optimus_config.allowed_chains:
132 | continue
133 | if optimus_config.target_investment_chains and chain_name.lower() not in optimus_config.target_investment_chains:
134 | print(f"WARNING: In the current setting, operability is restricted over {chain_name}")
135 |
136 | safe_info = safe_balances.get(chain_name, {})
137 | _print_status(f"Address ({chain_name})", safe_info.get('address', 'N/A'))
138 | _print_status(f"{safe_info.get('token', 'ETH')} Balance", safe_info.get('balance_formatted', 'N/A'))
139 |
140 | # Check for USDC balance on Principal Chain
141 | if chain_name.lower() == optimus_config.principal_chain:
142 | usdc_balance_formatted = safe_info.get('usdc_balance_formatted', 'N/A')
143 | _print_status("USDC Balance", usdc_balance_formatted)
144 |
145 | # Check for OLAS balance on Staking chain
146 | if chain_name.lower() == optimus_config.staking_chain:
147 | olas_balance_formatted = safe_info.get('olas_balance_formatted', 'N/A')
148 | _print_status("OLAS Balance", olas_balance_formatted)
149 |
150 | # Low balance check
151 | safe_threshold_wei = chain_config.get("chain_data", {}).get("user_params", {}).get("fund_requirements", {}).get("safe")
152 | if safe_threshold_wei:
153 | safe_threshold_eth = wei_to_eth(safe_threshold_wei)
154 | balance_str = safe_info.get('balance_formatted', '0').split()[0]
155 | try:
156 | current_balance = Decimal(balance_str)
157 | if current_balance < safe_threshold_eth:
158 | warning_msg = _warning_message(current_balance, safe_threshold_eth, f"Balance below threshold of {safe_threshold_eth:.5f} ETH")
159 | _print_status("Warning", warning_msg)
160 | except (ValueError, Exception):
161 | print(f"Warning: Could not parse balance '{balance_str}' for chain '{chain_name}'.")
162 | print()
163 | # Owner/Operator Section
164 | _print_subsection_header("Owner/Operator")
165 | _print_status("Address", operator_address)
166 | for chain_name, balance_info in wallet_info.get('operator_balances', {}).items():
167 | _print_status(f"{chain_name} Balance", balance_info.get('balance_formatted', 'N/A'))
168 |
169 | except Exception as e:
170 | print(f"An unexpected error occurred in generate_report: {e}")
171 |
172 | if __name__ == "__main__":
173 | generate_report()
174 |
--------------------------------------------------------------------------------
/run_service.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
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 | if [ "$(git rev-parse --is-inside-work-tree)" = true ]
22 | then
23 | # silently stop the existing service, if it exists
24 | chmod +x stop_service.sh && ./stop_service.sh > /dev/null 2>&1
25 | poetry install
26 | poetry run python run_service.py
27 | else
28 | echo "$directory is not a git repo!"
29 | exit 1
30 | fi
31 |
--------------------------------------------------------------------------------
/scripts/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # ------------------------------------------------------------------------------
3 | #
4 | # Copyright 2024 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 | """Helper scripts."""
21 |
--------------------------------------------------------------------------------
/scripts/fund.py:
--------------------------------------------------------------------------------
1 | """Fund an address on gnosis fork."""
2 |
3 | import json
4 | import os
5 | import sys
6 | from pathlib import Path
7 |
8 | from aea_ledger_ethereum.ethereum import EthereumApi, EthereumCrypto
9 | from dotenv import load_dotenv
10 |
11 |
12 | load_dotenv()
13 |
14 | RPC = os.environ.get("DEV_RPC", "http://localhost:8545")
15 |
16 | OLAS_CONTRACT_ADDRESS_GNOSIS = "0xcE11e14225575945b8E6Dc0D4F2dD4C570f79d9f"
17 |
18 |
19 | def fund(address: str, amount: float = 10.0) -> None:
20 | """Fund an address."""
21 | staking_keys_path = os.environ.get("STAKING_TEST_KEYS_PATH", None)
22 | ledger_api = EthereumApi(address=RPC)
23 | crypto = EthereumCrypto("scripts/keys/gnosis.txt")
24 | tx = ledger_api.get_transfer_transaction(
25 | sender_address=crypto.address,
26 | destination_address=address,
27 | amount=int(amount * 1e18),
28 | tx_fee=50000,
29 | tx_nonce="0x",
30 | chain_id=100,
31 | )
32 | stx = crypto.sign_transaction(transaction=tx)
33 | digest = ledger_api.send_signed_transaction(stx)
34 | ledger_api.get_transaction_receipt(tx_digest=digest, raise_on_try=True)
35 |
36 | print(f"Transferred: {ledger_api.get_balance(address=address)}")
37 | if staking_keys_path:
38 | staking_crypto = EthereumCrypto(staking_keys_path)
39 | with open(
40 | Path(
41 | "operate",
42 | "data",
43 | "contracts",
44 | "uniswap_v2_erc20",
45 | "build",
46 | "IUniswapV2ERC20.json",
47 | ),
48 | "r",
49 | encoding="utf-8",
50 | ) as abi_file:
51 | abi = json.load(abi_file)["abi"]
52 |
53 | olas_contract = ledger_api.api.eth.contract(
54 | address=ledger_api.api.to_checksum_address(OLAS_CONTRACT_ADDRESS_GNOSIS),
55 | abi=abi,
56 | )
57 |
58 | tx = olas_contract.functions.transfer(address, int(2e18)).build_transaction(
59 | {
60 | "chainId": 100,
61 | "gas": 100000,
62 | "gasPrice": ledger_api.api.to_wei("50", "gwei"),
63 | "nonce": ledger_api.api.eth.get_transaction_count(
64 | staking_crypto.address
65 | ),
66 | }
67 | )
68 |
69 | signed_txn = ledger_api.api.eth.account.sign_transaction(
70 | tx, staking_crypto.private_key
71 | )
72 | ledger_api.api.eth.send_raw_transaction(signed_txn.rawTransaction)
73 | balance = olas_contract.functions.balanceOf(address).call()
74 | print(f"Balance of {address} = {balance/1e18} OLAS")
75 |
76 |
77 | if __name__ == "__main__":
78 | fund(sys.argv[1])
79 |
--------------------------------------------------------------------------------
/scripts/keys/README.md:
--------------------------------------------------------------------------------
1 | ### This folder contains keys taken from hardhat nodes.
--------------------------------------------------------------------------------
/scripts/keys/gnosis.txt:
--------------------------------------------------------------------------------
1 | 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
--------------------------------------------------------------------------------
/scripts/setup_wallet.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # ------------------------------------------------------------------------------
3 | #
4 | # Copyright 2024 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 | """Setup wallet."""
21 |
22 | import requests
23 |
24 | from operate.types import ChainType
25 | from scripts.fund import fund
26 |
27 |
28 | print("Setting up user account")
29 | print(
30 | requests.post(
31 | "http://localhost:8000/api/account",
32 | json={
33 | "password": "password",
34 | },
35 | ).json()
36 | )
37 |
38 | print("Logging in")
39 | print(
40 | requests.post(
41 | "http://localhost:8000/api/account/login",
42 | json={
43 | "password": "password",
44 | },
45 | ).json()
46 | )
47 |
48 | wallet = requests.post(
49 | "http://localhost:8000/api/wallet",
50 | json={
51 | "chain_type": ChainType.GNOSIS,
52 | },
53 | ).json()
54 | print("Setting up wallet")
55 | print(wallet)
56 |
57 | print("Funding wallet: ", end="")
58 | fund(wallet["wallet"]["address"], amount=20)
59 |
60 | print(
61 | requests.post(
62 | "http://localhost:8000/api/wallet/safe",
63 | json={
64 | "chain_type": ChainType.GNOSIS,
65 | "owner": "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", # Backup owner
66 | },
67 | ).json()
68 | )
69 |
70 | print(
71 | requests.get(
72 | "http://localhost:8000/api/wallet",
73 | ).json()
74 | )
75 |
--------------------------------------------------------------------------------
/scripts/test_e2e.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | # ------------------------------------------------------------------------------
4 | #
5 | # Copyright 2021-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 | """This module contains e2e tests."""
21 |
22 | from pathlib import Path
23 |
24 | import requests
25 | from aea.helpers.yaml_utils import yaml_load
26 | from aea_ledger_ethereum.ethereum import EthereumApi, EthereumCrypto
27 |
28 |
29 | BASE_URL = "http://localhost:8000/api"
30 |
31 |
32 | def test_endpoint_e2e() -> None:
33 | """Test endpoint end to end"""
34 | with Path("templates/trader.yaml").open("r", encoding="utf-8") as stream:
35 | trader_template = yaml_load(stream=stream)
36 | phash = trader_template["hash"]
37 |
38 | print("Creating service using template")
39 |
40 | response = requests.post(
41 | url=f"{BASE_URL}/services",
42 | json={**trader_template, "deploy": True},
43 | ).json()
44 | print(response)
45 |
46 | input("> Press enter to stop")
47 | print(
48 | requests.post(
49 | url=f"{BASE_URL}/services/{phash}/deployment/stop",
50 | ).content.decode()
51 | )
52 |
53 | input("> Press enter to update")
54 |
55 | # Fund agent instance for swapping
56 | ledger_api = EthereumApi(address="http://localhost:8545")
57 | crypto = EthereumCrypto(".operate/key")
58 | (owner,) = response["chain_data"]["instances"]
59 | tx = ledger_api.get_transfer_transaction(
60 | sender_address=crypto.address,
61 | destination_address=owner,
62 | amount=1000000000000000,
63 | tx_fee=50000,
64 | tx_nonce="0x",
65 | chain_id=100,
66 | )
67 | stx = crypto.sign_transaction(transaction=tx)
68 | digest = ledger_api.send_signed_transaction(stx)
69 | ledger_api.get_transaction_receipt(tx_digest=digest)
70 |
71 | new_hash = "bafybeicxdpkuk5z5zfbkso7v5pywf4v7chxvluyht7dtgalg6dnhl7ejoe"
72 | print(
73 | requests.put(
74 | url=f"{BASE_URL}/services",
75 | json={
76 | "old_service_hash": trader_template["hash"],
77 | "new_service_hash": new_hash,
78 | "deploy": True,
79 | },
80 | ).content.decode()
81 | )
82 |
83 | input("> Press enter to stop")
84 | print(
85 | requests.post(
86 | url=f"{BASE_URL}/services/{new_hash}/deployment/stop",
87 | ).content.decode()
88 | )
89 |
90 |
91 | if __name__ == "__main__":
92 | test_endpoint_e2e()
93 |
--------------------------------------------------------------------------------
/scripts/test_staking_e2e.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | # ------------------------------------------------------------------------------
4 | #
5 | # Copyright 2021-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 | """This module contains e2e tests."""
21 |
22 | from pathlib import Path
23 |
24 | import requests
25 | from aea.helpers.yaml_utils import yaml_load
26 | from aea_ledger_ethereum.ethereum import EthereumApi, EthereumCrypto
27 |
28 |
29 | BASE_URL = "http://localhost:8000/api"
30 |
31 |
32 | def test_endpoint_e2e() -> None:
33 | """Test endpoint end to end"""
34 | with Path("templates/trader.yaml").open("r", encoding="utf-8") as stream:
35 | trader_template = yaml_load(stream=stream)
36 | phash = trader_template["hash"]
37 | trader_template["configuration"]["use_staking"] = True
38 |
39 | print("Creating service using template")
40 | response = requests.post(
41 | url=f"{BASE_URL}/services",
42 | json=trader_template,
43 | ).json()
44 | print(response)
45 |
46 | input("> Press enter to start")
47 | print(
48 | requests.get(
49 | url=f"{BASE_URL}/services/{phash}/deploy/",
50 | ).content.decode()
51 | )
52 |
53 | input("> Press enter to stop")
54 | print(
55 | requests.get(
56 | url=f"{BASE_URL}/services/{phash}/stop/",
57 | ).content.decode()
58 | )
59 |
60 | input("> Press enter to update")
61 | # Fund agent instance for swapping
62 | ledger_api = EthereumApi(address="http://localhost:8545")
63 | crypto = EthereumCrypto(".operate/key")
64 | (owner,) = response["chain_data"]["instances"]
65 | tx = ledger_api.get_transfer_transaction(
66 | sender_address=crypto.address,
67 | destination_address=owner,
68 | amount=1000000000000000,
69 | tx_fee=50000,
70 | tx_nonce="0x",
71 | chain_id=100,
72 | )
73 | stx = crypto.sign_transaction(transaction=tx)
74 | digest = ledger_api.send_signed_transaction(stx)
75 | ledger_api.get_transaction_receipt(tx_digest=digest)
76 |
77 | old = trader_template["hash"]
78 | trader_template[
79 | "hash"
80 | ] = "bafybeicxdpkuk5z5zfbkso7v5pywf4v7chxvluyht7dtgalg6dnhl7ejoe"
81 | print(
82 | requests.put(
83 | url=f"{BASE_URL}/services",
84 | json={
85 | "old": old,
86 | "new": trader_template,
87 | },
88 | ).content.decode()
89 | )
90 |
91 |
92 | if __name__ == "__main__":
93 | test_endpoint_e2e()
94 |
--------------------------------------------------------------------------------
/scripts/transfer_olas.py:
--------------------------------------------------------------------------------
1 | """
2 | Script for transferring OLAS
3 |
4 | Usage:
5 | python transfer_olas.py PATH_TO_KEY_CONTAINING_OLAS ADDRESS_TO_TRANSFER AMOUNT
6 |
7 | Example:
8 | python transfer_olas.py keys/gnosis.txt 0xce11e14225575945b8e6dc0d4f2dd4c570f79d9f 2
9 | """
10 |
11 | import json
12 | import os
13 | import sys
14 | from pathlib import Path
15 |
16 | from aea_ledger_ethereum.ethereum import EthereumApi, EthereumCrypto
17 |
18 |
19 | RPC = os.environ.get("DEV_RPC", "http://localhost:8545")
20 | OLAS_CONTRACT_ADDRESS_GNOSIS = "0xcE11e14225575945b8E6Dc0D4F2dD4C570f79d9f"
21 | WEI_MULTIPLIER = 1e18
22 |
23 |
24 | def fund(wallet: str, address: str, amount: float = 20.0) -> None:
25 | """Fund wallet with OLAS token"""
26 | staking_wallet = EthereumCrypto(wallet)
27 | ledger_api = EthereumApi(address=RPC)
28 | olas_contract = ledger_api.api.eth.contract(
29 | address=ledger_api.api.to_checksum_address(OLAS_CONTRACT_ADDRESS_GNOSIS),
30 | abi=json.loads(
31 | Path(
32 | "operate",
33 | "data",
34 | "contracts",
35 | "uniswap_v2_erc20",
36 | "build",
37 | "IUniswapV2ERC20.json",
38 | ).read_text(encoding="utf-8")
39 | ).get("abi"),
40 | )
41 | print(
42 | f"Balance of {address} = {olas_contract.functions.balanceOf(address).call()/1e18} OLAS"
43 | )
44 | print(f"Transferring {amount} OLAS from {staking_wallet.address} to {address}")
45 |
46 | tx = olas_contract.functions.transfer(
47 | address, int(amount * WEI_MULTIPLIER)
48 | ).build_transaction(
49 | {
50 | "chainId": 100,
51 | "gas": 100000,
52 | "gasPrice": ledger_api.api.to_wei("50", "gwei"),
53 | "nonce": ledger_api.api.eth.get_transaction_count(staking_wallet.address),
54 | }
55 | )
56 |
57 | signed_txn = ledger_api.api.eth.account.sign_transaction(
58 | tx, staking_wallet.private_key
59 | )
60 | ledger_api.api.eth.send_raw_transaction(signed_txn.rawTransaction)
61 | print(
62 | f"Balance of {address} = {olas_contract.functions.balanceOf(address).call()/1e18} OLAS"
63 | )
64 |
65 |
66 | if __name__ == "__main__":
67 | args = sys.argv[1:]
68 | if len(args) == 2:
69 | fund(wallet=args[0], address=args[1])
70 | sys.exit()
71 |
72 | if len(args) == 3:
73 | fund(wallet=args[0], address=args[1], amount=float(args[2]))
74 | sys.exit()
75 |
76 | print(
77 | """Script for transferring OLAS
78 |
79 | Usage:
80 | python transfer_olas.py PATH_TO_KEY_CONTAINING_OLAS ADDRESS_TO_TRANSFER AMOUNT
81 |
82 | Example:
83 | python transfer_olas.py keys/gnosis.txt 0xce11e14225575945b8e6dc0d4f2dd4c570f79d9f 2"""
84 | )
85 |
--------------------------------------------------------------------------------
/staking_report.py:
--------------------------------------------------------------------------------
1 | # staking_report.py
2 | import warnings
3 | warnings.filterwarnings("ignore", category=UserWarning)
4 | import json
5 | import math
6 | from pathlib import Path
7 | from web3 import Web3, HTTPProvider
8 | from decimal import Decimal, getcontext
9 | import logging
10 |
11 | from run_service import (
12 | get_service_template,
13 | FALLBACK_STAKING_PARAMS,
14 | CHAIN_ID_TO_METADATA,
15 | OPERATE_HOME,
16 | )
17 |
18 | from operate.ledger.profiles import STAKING
19 | from operate.types import ChainType
20 |
21 | from utils import (
22 | _print_section_header,
23 | _print_subsection_header,
24 | _print_status,
25 | wei_to_olas,
26 | wei_to_eth,
27 | _warning_message,
28 | StakingState,
29 | get_chain_name,
30 | load_operator_safe_balance,
31 | validate_config,
32 | _color_bool,
33 | _color_string,
34 | ColorCode
35 |
36 | )
37 |
38 | # Set decimal precision
39 | getcontext().prec = 18
40 |
41 | # Configure logging
42 | logging.basicConfig(level=logging.INFO, format='%(message)s')
43 |
44 | SCRIPT_PATH = Path(__file__).resolve().parent
45 | STAKING_TOKEN_JSON_PATH = SCRIPT_PATH / "contracts" / "StakingToken.json"
46 | ACTIVITY_CHECKER_JSON_PATH = SCRIPT_PATH / "contracts" / "StakingActivityChecker.json"
47 | SERVICE_REGISTRY_TOKEN_UTILITY_JSON_PATH = SCRIPT_PATH / "contracts" / "ServiceRegistryTokenUtility.json"
48 |
49 | def staking_report(config: dict) -> None:
50 | try:
51 | _print_section_header("Performance")
52 |
53 | operator_address = load_operator_safe_balance(OPERATE_HOME)
54 | if not operator_address:
55 | print("Error: Operator address could not be loaded.")
56 | return
57 |
58 | # Find the chain configuration where use_staking is True
59 | chain_data = next(
60 | (
61 | data for data in config.get("chain_configs", {}).values()
62 | if data.get("chain_data", {}).get("user_params", {}).get("use_staking")
63 | ),
64 | None
65 | )
66 | if not chain_data:
67 | return
68 |
69 | _print_subsection_header("Staking")
70 |
71 | rpc = chain_data.get("ledger_config", {}).get("rpc")
72 | if not rpc:
73 | print("Error: RPC endpoint not found in ledger configuration.")
74 | return
75 |
76 | staking_program_id = chain_data.get("chain_data", {}).get("user_params", {}).get("staking_program_id")
77 | if not staking_program_id:
78 | print("Error: 'staking_program_id' not found in user parameters.")
79 | return
80 |
81 | home_chain_id = config.get("home_chain_id")
82 | if not home_chain_id:
83 | print("Error: 'home_chain_id' not found in config.")
84 | return
85 |
86 | service_id = config.get("chain_configs", {}).get(str(home_chain_id), {}).get("chain_data", {}).get("token")
87 | if not service_id:
88 | print(f"Error: 'token' not found in chain data for chain ID {home_chain_id}.")
89 | return
90 |
91 | multisig_address = config.get("chain_configs", {}).get(str(home_chain_id), {}).get("chain_data", {}).get("multisig")
92 | if not multisig_address:
93 | print(f"Error: 'multisig' address not found in chain data for chain ID {home_chain_id}.")
94 | return
95 |
96 | w3 = Web3(HTTPProvider(rpc))
97 |
98 | home_chain_type = ChainType.from_id(int(home_chain_id))
99 | staking_token_address = STAKING.get(home_chain_type, {}).get("optimus_alpha")
100 | if not staking_token_address:
101 | print("Error: Staking token address not found.")
102 | return
103 |
104 | # Load ABI files
105 | with open(STAKING_TOKEN_JSON_PATH, "r", encoding="utf-8") as file:
106 | staking_token_data = json.load(file)
107 | staking_token_abi = staking_token_data.get("abi", [])
108 | staking_token_contract = w3.eth.contract(
109 | address=staking_token_address, abi=staking_token_abi # type: ignore
110 | )
111 |
112 | # Get staking state
113 | staking_state_value = staking_token_contract.functions.getStakingState(service_id).call()
114 | staking_state = StakingState(staking_state_value)
115 | is_staked = staking_state in (StakingState.STAKED, StakingState.EVICTED)
116 | _print_status("Is service staked?", _color_bool(is_staked, "Yes", "No"))
117 | if is_staked:
118 | _print_status("Staking program",("modius_"+ str(staking_program_id) + " " +str(home_chain_type).rsplit('.', maxsplit=1)[-1]))
119 | _print_status("Staking state", staking_state.name if staking_state == StakingState.STAKED else _color_string(staking_state.name, ColorCode.RED))
120 |
121 | # Activity Checker
122 | activity_checker_address = staking_token_contract.functions.activityChecker().call()
123 | with open(ACTIVITY_CHECKER_JSON_PATH, "r", encoding="utf-8") as file:
124 | activity_checker_data = json.load(file)
125 | activity_checker_abi = activity_checker_data.get("abi", [])
126 | activity_checker_contract = w3.eth.contract(
127 | address=activity_checker_address, abi=activity_checker_abi # type: ignore
128 | )
129 |
130 | # Service Registry Token Utility
131 | with open(SERVICE_REGISTRY_TOKEN_UTILITY_JSON_PATH, "r", encoding="utf-8") as file:
132 | service_registry_token_utility_data = json.load(file)
133 | service_registry_token_utility_contract_address = staking_token_contract.functions.serviceRegistryTokenUtility().call()
134 | service_registry_token_utility_abi = service_registry_token_utility_data.get("abi", [])
135 | service_registry_token_utility_contract = w3.eth.contract(
136 | address=service_registry_token_utility_contract_address,
137 | abi=service_registry_token_utility_abi,
138 | )
139 |
140 | # Get security deposit
141 | security_deposit = service_registry_token_utility_contract.functions.getOperatorBalance(
142 | operator_address, service_id
143 | ).call()
144 |
145 | # Get agent bond
146 | agent_ids = FALLBACK_STAKING_PARAMS.get("agent_ids", [])
147 | if not agent_ids:
148 | print("Error: 'agent_ids' not found in FALLBACK_STAKING_PARAMS.")
149 | return
150 | agent_bond = service_registry_token_utility_contract.functions.getAgentBond(
151 | service_id, agent_ids[0]
152 | ).call()
153 |
154 | min_staking_deposit = staking_token_contract.functions.minStakingDeposit().call()
155 | min_security_deposit = min_staking_deposit
156 |
157 | security_deposit_formatted = wei_to_olas(security_deposit)
158 | agent_bond_formatted = wei_to_olas(agent_bond)
159 | min_staking_deposit_formatted = wei_to_olas(min_staking_deposit)
160 |
161 | security_deposit_decimal = Decimal(security_deposit_formatted.split()[0])
162 | min_security_deposit_decimal = Decimal(min_staking_deposit_formatted.split()[0])
163 |
164 | agent_bond_decimal = Decimal(agent_bond_formatted.split()[0])
165 |
166 | _print_status(
167 | "Staked (security deposit)",
168 | security_deposit_formatted,
169 | _warning_message(security_deposit_decimal, min_security_deposit_decimal)
170 | )
171 | _print_status(
172 | "Staked (agent bond)",
173 | agent_bond_formatted,
174 | _warning_message(agent_bond_decimal, min_security_deposit_decimal)
175 | )
176 |
177 | # Accrued rewards
178 | service_info = staking_token_contract.functions.mapServiceInfo(service_id).call()
179 | rewards = service_info[3]
180 | _print_status("Accrued rewards", wei_to_olas(rewards))
181 |
182 | # Liveness ratio and transactions
183 | liveness_ratio = activity_checker_contract.functions.livenessRatio().call()
184 | multisig_nonces_24h_threshold = math.ceil(
185 | (liveness_ratio * 60 * 60 * 24) / Decimal(1e18)
186 | )
187 |
188 | multisig_nonces = activity_checker_contract.functions.getMultisigNonces(multisig_address).call()
189 | multisig_nonces = multisig_nonces[0]
190 | service_info = staking_token_contract.functions.getServiceInfo(service_id).call()
191 | multisig_nonces_on_last_checkpoint = service_info[2][0]
192 | multisig_nonces_since_last_cp = multisig_nonces - multisig_nonces_on_last_checkpoint
193 | multisig_nonces_current_epoch = multisig_nonces_since_last_cp
194 | _print_status(
195 | f"{str(home_chain_type).rsplit('.', maxsplit=1)[-1]} txs in current epoch ",
196 | str(multisig_nonces_current_epoch),
197 | _warning_message(
198 | Decimal(multisig_nonces_current_epoch),
199 | Decimal(multisig_nonces_24h_threshold),
200 | f"- Too low. Threshold is {multisig_nonces_24h_threshold}."
201 | )
202 | )
203 |
204 | except Exception as e:
205 | print(f"An unexpected error occurred in staking_report: {e}")
206 |
207 | if __name__ == "__main__":
208 | try:
209 | # Load configuration
210 | config = load_config()
211 | if not config:
212 | print("Error: Config is empty.")
213 | else:
214 | staking_report(config)
215 | except Exception as e:
216 | print(f"An unexpected error occurred: {e}")
217 |
--------------------------------------------------------------------------------
/stop_service.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # ------------------------------------------------------------------------------
3 | #
4 | # Copyright 2024 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 | """Olas Modius Quickstart script."""
20 | import warnings
21 | warnings.filterwarnings("ignore", category=UserWarning)
22 |
23 | import sys
24 |
25 | from operate.cli import OperateApp
26 | from run_service import (
27 | print_title, OPERATE_HOME, load_local_config, get_service_template, print_section, get_service,
28 | )
29 |
30 |
31 | def main() -> None:
32 | """Run service."""
33 |
34 | print_title("Stop Olas Modius Quickstart")
35 |
36 | operate = OperateApp(
37 | home=OPERATE_HOME,
38 | )
39 | operate.setup()
40 |
41 | # check if optimus was started before
42 | path = OPERATE_HOME / "local_config.json"
43 | if not path.exists():
44 | print("Nothing to clean. Exiting.")
45 | sys.exit(0)
46 |
47 | optimus_config = load_local_config()
48 | template = get_service_template(optimus_config)
49 | manager = operate.service_manager()
50 | service = get_service(manager, template)
51 | manager.stop_service_locally(hash=service.hash, delete=True)
52 |
53 | print()
54 | print_section("Service stopped")
55 |
56 |
57 | if __name__ == "__main__":
58 | main()
59 |
--------------------------------------------------------------------------------
/stop_service.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
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 | if [ "$(git rev-parse --is-inside-work-tree)" = true ]
22 | then
23 | poetry install
24 | poetry run python stop_service.py || echo "Stopping the deployment failed. Continuing with cleanup."
25 |
26 | # remove all containers with the name optimus, if they exist
27 | container_ids=$(docker ps -a -q --filter name=optimus)
28 | if [ -n "$container_ids" ]; then
29 | docker rm -f $container_ids
30 | fi
31 |
32 | # remove old deployments if they exist
33 | if ls .olas-modius/services/*/deployment 1> /dev/null 2>&1; then
34 | sudo rm -rf .olas-modius/services/*/deployment
35 | fi
36 | else
37 | echo "$directory is not a git repo!"
38 | exit 1
39 | fi
40 |
--------------------------------------------------------------------------------
/suggest_funding_report.py:
--------------------------------------------------------------------------------
1 | import warnings
2 | warnings.filterwarnings("ignore", category=UserWarning)
3 |
4 | import json
5 | from datetime import datetime
6 | import logging
7 | from decimal import Decimal, getcontext, ROUND_UP
8 | from pathlib import Path
9 | from typing import Any, Tuple, Dict
10 | from web3 import Web3
11 |
12 | from run_service import (
13 | load_local_config,
14 | CHAIN_ID_TO_METADATA,
15 | OPERATE_HOME
16 | )
17 |
18 | from utils import (
19 | _print_subsection_header,
20 | _print_status,
21 | get_chain_name,
22 | load_operator_address,
23 | validate_config,
24 | )
25 |
26 | from wallet_info import save_wallet_info, load_config as load_wallet_config
27 |
28 | # Set decimal precision
29 | getcontext().prec = 18
30 | GAS_COSTS_JSON_PATH = OPERATE_HOME / "gas_costs.json"
31 | FUNDING_MULTIPLIER = Decimal(10)
32 | ROUNDING_PRECISION = Decimal('0.0001')
33 | MIN_TRANSACTIONS_SUPPORTED = 5
34 | # Configure logging
35 | logging.basicConfig(level=logging.INFO, format='%(message)s')
36 |
37 | class ColorCode:
38 | """Color code"""
39 | GREEN = "\033[92m"
40 | RED = "\033[91m"
41 | YELLOW = "\033[93m"
42 | RESET = "\033[0m"
43 |
44 | def load_wallet_info() -> dict:
45 | """Load wallet info from file."""
46 | save_wallet_info()
47 | file_path = OPERATE_HOME / "wallets" / "wallet_info.json"
48 | return _load_json_file(file_path, "Wallet info")
49 |
50 | def load_gas_costs(file_path: Path) -> dict:
51 | """Load gas costs details from file."""
52 | try:
53 | return _load_json_file(file_path, "Gas costs")
54 | except Exception as e:
55 | print("No transactions have been recorded until now")
56 | return {}
57 |
58 | def generate_gas_cost_report():
59 | """Generate and print the gas cost report."""
60 | try:
61 | gas_costs = load_gas_costs(GAS_COSTS_JSON_PATH)
62 | wallet_info = load_wallet_info()
63 | optimus_config = load_local_config()
64 | if not wallet_info:
65 | print("Error: Wallet info is empty.")
66 | return
67 |
68 | operator_address = load_operator_address(OPERATE_HOME)
69 | if not operator_address:
70 | print("Error: Operator address could not be loaded.")
71 | return
72 |
73 | config = load_wallet_config()
74 | if not config:
75 | print("Error: Config is empty.")
76 | return
77 |
78 | if not validate_config(config):
79 | return
80 |
81 | _print_report_header()
82 |
83 | for chain_id, _ in config.get("chain_configs", {}).items():
84 | chain_name = get_chain_name(chain_id, CHAIN_ID_TO_METADATA)
85 | if optimus_config.allowed_chains and chain_name.lower() not in optimus_config.allowed_chains:
86 | continue
87 | if optimus_config.target_investment_chains and chain_name.lower() not in optimus_config.target_investment_chains:
88 | print(f"WARNING: In the current setting, operability is restricted over {chain_name}")
89 | continue
90 |
91 | balance_info = wallet_info.get('main_wallet_balances', {}).get(chain_name, {})
92 | agent_address = wallet_info.get('main_wallet_address', 'N/A')
93 | chain_rpc = wallet_info.get("chain_configs").get(str(chain_id)).get('rpc')
94 | analyze_and_report_gas_costs(gas_costs, balance_info, chain_id, chain_name, agent_address, chain_rpc)
95 |
96 | except Exception as e:
97 | print(f"An unexpected error occurred in generate_gas_cost_report: {e}")
98 |
99 | def _load_json_file(file_path: Path, description: str) -> dict:
100 | """Helper function to load JSON data from a file."""
101 | try:
102 | with open(file_path, "r", encoding="utf-8") as file:
103 | return json.load(file)
104 | except json.JSONDecodeError:
105 | print(f"Error: {description} file contains invalid JSON.")
106 | return {}
107 |
108 | return {}
109 |
110 | def analyze_and_report_gas_costs(gas_costs: dict, balance_info: Any, chain_id: int, chain_name: str, agent_address: str, chain_rpc: str) -> None:
111 | """Analyze gas costs and suggest funding amount."""
112 | _print_subsection_header(f"Funding Recommendation for {chain_name}")
113 |
114 | transactions = gas_costs.get(chain_id, [])
115 | average_gas_price = _calculate_average_gas_price(chain_rpc)
116 | if average_gas_price is None:
117 | print(f"Unable to calculate gas fees for chain_name {chain_name}")
118 | return
119 | if not transactions:
120 | average_gas_used = 3_00_000
121 | else:
122 | total_gas_used = sum(Decimal(tx["gas_used"]) for tx in transactions)
123 | average_gas_used = total_gas_used / Decimal(len(transactions))
124 |
125 | average_gas_cost = average_gas_used * average_gas_price
126 | funding_needed, funding_suggestion = _calculate_funding_needed(average_gas_cost, balance_info)
127 | _report_funding_status(chain_name, balance_info, average_gas_cost, average_gas_price, funding_suggestion, funding_needed, agent_address)
128 |
129 | def _calculate_average_gas_price(rpc, fee_history_blocks = 500000, fee_history_percentile = 50) -> Decimal:
130 | web3 = Web3(Web3.HTTPProvider(rpc))
131 | block_number = web3.eth.block_number
132 | fee_history = web3.eth.fee_history(
133 | fee_history_blocks, block_number, [fee_history_percentile]
134 | )
135 | base_fees = fee_history.get('baseFeePerGas')
136 | if base_fees is None:
137 | base_fees = [20000000000] #fallback base fee
138 |
139 | priority_fees = [reward[0] for reward in fee_history.get('reward',[]) if reward]
140 | if not priority_fees:
141 | priority_fees = [2000000000] #fallback priority fee
142 |
143 | # Calculate average fees
144 | average_base_fee = sum(base_fees) / len(base_fees)
145 | average_priority_fee = sum(priority_fees) / len(priority_fees)
146 |
147 | average_gas_price = average_base_fee + average_priority_fee
148 | adjusted_gas_price = average_gas_price * 1.5
149 | return Decimal(adjusted_gas_price)
150 |
151 | def _calculate_funding_needed(average_gas_cost: Decimal, balance_info: Any) -> Tuple[Decimal,Decimal]:
152 | """Calculate the funding needed based on average gas cost and current balance."""
153 | funding_suggestion = Decimal(average_gas_cost) * FUNDING_MULTIPLIER / Decimal(1e18)
154 | return max(funding_suggestion - Decimal(balance_info.get('balance', 0)), Decimal(0)), funding_suggestion
155 |
156 | def _report_funding_status(chain_name: str, balance_info: Any, average_gas_cost: Decimal, average_gas_price: Decimal, funding_suggestion: Decimal, funding_needed: Decimal, agent_address: str):
157 | """Report the funding status and suggestions."""
158 | _print_status(f"[{chain_name}] Current Balance ", balance_info.get('balance_formatted', 'N/A'))
159 | _print_status(f"[{chain_name}] Average Gas Cost (WEI) ", average_gas_cost)
160 | _print_status(f"[{chain_name}] Average Gas Price (WEI) ", average_gas_price)
161 | _print_status(f"[{chain_name}] Amount of ETH to cover for estimated gas cost of the next {FUNDING_MULTIPLIER} Transactions: ", f"{funding_suggestion} ETH")
162 |
163 | average_gas_cost_eth = average_gas_cost / Decimal(1e18)
164 | current_balance = Decimal(balance_info.get('balance', 0))
165 | transactions_supported = current_balance / average_gas_cost_eth
166 |
167 | if funding_needed <= 0:
168 | funding_message = f"[{chain_name}] Current balance can cover for the gas cost of up to {int(transactions_supported)} transactions"
169 | print(_color_string(funding_message, ColorCode.GREEN))
170 | elif transactions_supported < MIN_TRANSACTIONS_SUPPORTED:
171 | funding_needed_rounded = _round_up(funding_needed, ROUNDING_PRECISION)
172 | funding_message = f"[{chain_name}] BALANCE TOO LOW! Please urgently fund your agent {agent_address} with at least {funding_needed_rounded} ETH to ensure smooth operation"
173 | print(_color_string(funding_message, ColorCode.RED))
174 | else:
175 | funding_needed_rounded = _round_up(funding_needed, ROUNDING_PRECISION)
176 | funding_message = f"[{chain_name}] Please fund your agent {agent_address} with at least {funding_needed_rounded} ETH to cover future transaction costs"
177 | print(_color_string(funding_message, ColorCode.YELLOW))
178 |
179 | def _round_up(value: Decimal, precision: Decimal) -> Decimal:
180 | """Round up a Decimal value to a specified precision."""
181 | return value.quantize(precision, rounding=ROUND_UP)
182 |
183 | def _color_string(text: str, color_code: str) -> str:
184 | return f"{color_code}{text}{ColorCode.RESET}"
185 |
186 | def _print_report_header():
187 | """Print the header for the gas cost report."""
188 | print("\n==============")
189 | print("Suggested Funding Report")
190 | print("Generated on: ", datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
191 | print("==============")
192 |
193 | if __name__ == "__main__":
194 | generate_gas_cost_report()
--------------------------------------------------------------------------------
/templates/superfest.yaml:
--------------------------------------------------------------------------------
1 | name: "Superfest Agent"
2 | description: "A single-agent service (sovereign agent) placing bets on Omen"
3 | hash: bafybeiegxvr7uzl6srgq7x36escs4jrc35cnk73ky23egfgkarp7hz6x4u
4 | image: https://operate.olas.network/_next/image?url=%2Fimages%2Fprediction-agent.png&w=3840&q=75
5 | service_version: v0.18.1
6 | home_chain_id: 10
7 | configurations:
8 | 100:
9 | staking_program_id: pearl_beta
10 | nft: bafybeig64atqaladigoc3ds4arltdu63wkdrk3gesjfvnfdmz35amv7faq
11 | rpc: http://localhost:8545 # User provided
12 | threshold: 1 # TODO: Move to service component
13 | use_staking: false # User provided
14 | cost_of_bond: 10000000000000000
15 | monthly_gas_estimate: 10000000000000000000 # TODO: Where is this used
16 | fund_requirements:
17 | agent: 100000000000000000
18 | safe: 5000000000000000000
19 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
1 | """This package contains tests for the Valory services."""
2 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist = isort
3 | skip_missing_interpreters = true
4 | isolated_build = True
5 |
6 |
7 | [testenv:bandit]
8 | skipsdist = True
9 | skip_install = True
10 | deps =
11 | tomte[bandit]==0.2.15
12 | commands =
13 | bandit -r operate -x */tests/*
14 | bandit -s B101 -r tests scripts
15 |
16 | [testenv:black]
17 | skipsdist = True
18 | skip_install = True
19 | deps =
20 | tomte[black]==0.2.15
21 | commands =
22 | black operate scripts tests
23 |
24 | [testenv:black-check]
25 | skipsdist = True
26 | skip_install = True
27 | deps =
28 | tomte[black]==0.2.15
29 | commands =
30 | black --check operate scripts tests
31 |
32 | [testenv:isort]
33 | skipsdist = True
34 | skip_install = True
35 | deps =
36 | tomte[isort]==0.2.15
37 | commands =
38 | isort operate/ scripts/ tests/
39 |
40 | [testenv:isort-check]
41 | skipsdist = True
42 | skip_install = True
43 | deps =
44 | tomte[isort]==0.2.15
45 | commands =
46 | isort --check-only --gitignore operate/ scripts/ tests/
47 |
48 | [testenv:flake8]
49 | skipsdist = True
50 | skip_install = True
51 | deps =
52 | tomte[flake8]==0.2.15
53 | commands =
54 | flake8 operate scripts tests
55 |
56 | [testenv:mypy]
57 | skipsdist = True
58 | skip_install = True
59 | deps =
60 | tomte[mypy]==0.2.15
61 | commands =
62 | mypy operate/ tests/ scripts/ --disallow-untyped-defs --config-file tox.ini
63 |
64 | [testenv:pylint]
65 | whitelist_externals = /bin/sh
66 | skipsdist = True
67 | deps =
68 | tomte[pylint]==0.2.15
69 | commands =
70 | pylint operate scripts -j 0 --rcfile=.pylintrc
71 |
72 | [testenv:safety]
73 | skipsdist = True
74 | skip_install = True
75 | deps =
76 | tomte[safety]==0.2.15
77 | commands =
78 | safety check -i 37524 -i 38038 -i 37776 -i 38039 -i 39621 -i 40291 -i 39706 -i 41002 -i 51358 -i 51499 -i 67599 -i 70612
79 |
80 | [testenv:vulture]
81 | skipsdist = True
82 | skip_install = True
83 | deps =
84 | tomte[vulture]==0.2.15
85 | commands =
86 | vulture operate/services scripts/whitelist.py
87 |
88 | [testenv:darglint]
89 | skipsdist = True
90 | skip_install = True
91 | deps =
92 | tomte[darglint]==0.2.15
93 | commands =
94 | darglint operate scripts tests
95 |
96 |
97 | [testenv:check-copyright]
98 | skipsdist = True
99 | skip_install = True
100 | deps =
101 | commands =
102 | {toxinidir}/scripts/check_copyright.py --check
103 |
104 | [testenv:fix-copyright]
105 | skipsdist = True
106 | skip_install = True
107 | deps =
108 | commands =
109 | {toxinidir}/scripts/check_copyright.py
110 |
111 | [testenv:liccheck]
112 | skipsdist = True
113 | usedevelop = True
114 | deps = tomte[liccheck,cli]==0.2.15
115 | commands =
116 | tomte freeze-dependencies --output-path {envtmpdir}/requirements.txt
117 | liccheck -s tox.ini -r {envtmpdir}/requirements.txt -l PARANOID
118 |
119 | [darglint]
120 | docstring_style=sphinx
121 | strictness=short
122 | ignore_regex=async_act
123 | ignore=DAR401
124 |
125 | [flake8]
126 | paths=autonomy,packages,scripts,tests
127 | exclude=.md,
128 | *_pb2.py,
129 | autonomy/__init__.py,
130 | custom_types.py,
131 | *_pb2_grpc.py,
132 | packages/valory/connections/http_client,
133 | packages/valory/connections/ledger,
134 | packages/valory/connections/p2p_libp2p_client,
135 | packages/valory/protocols/acn,
136 | packages/valory/protocols/contract_api,
137 | packages/valory/protocols/http,
138 | packages/valory/protocols/ledger_api
139 | max-line-length = 88
140 | select = B,C,D,E,F,I,W,
141 | ignore = E203,E501,W503,D202,B014,D400,D401,DAR,B028,B017
142 | application-import-names = autonomy,packages,tests,scripts
143 |
144 | [isort]
145 | # for black compatibility
146 | multi_line_output=3
147 | include_trailing_comma=True
148 | force_grid_wrap=0
149 | use_parentheses=True
150 | ensure_newline_before_comments = True
151 | line_length=88
152 | # custom configurations
153 | order_by_type=False
154 | case_sensitive=True
155 | lines_after_imports=2
156 | skip =
157 | skip_glob =
158 | known_first_party=operate
159 | known_local_folder=tests
160 | sections=FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER
161 |
162 | [mypy]
163 | strict_optional = True
164 |
165 | [mypy-typing_extentions.*]
166 | ignore_missing_imports = True
167 |
168 | [mypy-requests.*]
169 | ignore_missing_imports = True
170 |
171 | [mypy-aea.*]
172 | ignore_missing_imports = True
173 |
174 | [mypy-aea_ledger_ethereum.*]
175 | ignore_missing_imports = True
176 |
177 | [mypy-dotenv.*]
178 | ignore_missing_imports = True
179 |
180 | [mypy-autonomy.*]
181 | ignore_missing_imports = True
182 |
183 | [mypy-hexbytes.*]
184 | ignore_missing_imports = True
185 |
186 | [mypy-starlette.*]
187 | ignore_missing_imports = True
188 |
189 | [mypy-aea_cli_ipfs.*]
190 | ignore_missing_imports = True
191 |
192 | [mypy-clea.*]
193 | ignore_missing_imports = True
194 |
195 | [mypy-uvicorn.*]
196 | ignore_missing_imports = True
197 |
198 | [mypy-fastapi.*]
199 | ignore_missing_imports = True
200 |
201 | [mypy-web3.*]
202 | ignore_missing_imports = True
203 |
204 | [mypy-docker.*]
205 | ignore_missing_imports = True
206 |
207 | [mypy-compose.*]
208 | ignore_missing_imports = True
209 |
210 | [mypy-flask.*]
211 | ignore_missing_imports = True
212 |
213 | [mypy-werkzeug.*]
214 | ignore_missing_imports = True
215 |
216 | [mypy-psutil.*]
217 | ignore_missing_imports = True
218 |
--------------------------------------------------------------------------------
/utils.py:
--------------------------------------------------------------------------------
1 | # utils.py
2 | import json
3 | from pathlib import Path
4 | from datetime import datetime
5 | from decimal import Decimal, getcontext
6 | import logging
7 | import docker
8 | from web3 import Web3
9 | from web3.middleware import geth_poa_middleware
10 | from enum import Enum
11 | from operate.cli import OperateApp
12 | import yaml
13 | # Set decimal precision
14 | getcontext().prec = 18
15 |
16 | # Configure logging
17 | logging.basicConfig(level=logging.INFO, format='%(message)s')
18 |
19 | # Terminal color codes
20 | class ColorCode:
21 | GREEN = "\033[92m"
22 | RED = "\033[91m"
23 | YELLOW = "\033[93m"
24 | RESET = "\033[0m"
25 |
26 | class StakingState(Enum):
27 | """Staking state enumeration for the staking."""
28 | UNSTAKED = 0
29 | STAKED = 1
30 | EVICTED = 2
31 |
32 | from run_service import (
33 | load_local_config,
34 | get_service_template,
35 | get_service,
36 | OPERATE_HOME
37 | )
38 |
39 | def _color_string(text: str, color_code: str) -> str:
40 | return f"{color_code}{text}{ColorCode.RESET}"
41 |
42 | def _color_bool(is_true: bool, true_string: str = "True", false_string: str = "False") -> str:
43 | if is_true:
44 | return _color_string(true_string, ColorCode.GREEN)
45 | return _color_string(false_string, ColorCode.RED)
46 |
47 | def _warning_message(current_value: Decimal, threshold: Decimal, message: str = "") -> str:
48 | default_message = _color_string(
49 | f"- Value too low. Threshold is {threshold:.2f}.",
50 | ColorCode.YELLOW,
51 | )
52 | if current_value < threshold:
53 | return _color_string(message or default_message, ColorCode.YELLOW)
54 | return ""
55 |
56 | def _print_section_header(header: str, output_width: int = 80) -> None:
57 | print("\n\n" + header)
58 | print("=" * output_width)
59 |
60 | def _print_subsection_header(header: str, output_width: int = 80) -> None:
61 | print("\n" + header)
62 | print("-" * output_width)
63 |
64 | def _print_status(key: str, value: str, message: str = "") -> None:
65 | line = f"{key:<30}{value:<20}"
66 | if message:
67 | line += f"{message}"
68 | print(line)
69 |
70 | def wei_to_unit(wei: int) -> Decimal:
71 | """Convert Wei to unit."""
72 | return Decimal(wei) / Decimal(1e18)
73 |
74 | def wei_to_token(wei: int, token: str = "xDAI") -> str:
75 | """Convert Wei to token."""
76 | return f"{wei_to_unit(wei):.2f} {token}"
77 |
78 | def wei_to_olas(wei: int) -> str:
79 | """Converts and formats wei to OLAS."""
80 | return "{:.2f} OLAS".format(wei_to_unit(wei))
81 |
82 | def wei_to_eth(wei_value):
83 | return Decimal(wei_value) / Decimal(1e18)
84 |
85 | def get_chain_name(chain_id, chain_id_to_metadata):
86 | return chain_id_to_metadata.get(int(chain_id), {}).get("name", f"Chain {chain_id}")
87 |
88 | def load_operator_address(operate_home):
89 | ethereum_json_path = operate_home / "wallets" / "ethereum.json"
90 | try:
91 | with open(ethereum_json_path, "r") as f:
92 | ethereum_data = json.load(f)
93 | operator_address = ethereum_data.get("address", None)
94 | if not operator_address:
95 | print("Error: Operator address not found in the wallet file.")
96 | return None
97 | return operator_address
98 | except FileNotFoundError:
99 | print(f"Error: Ethereum wallet file not found at {ethereum_json_path}")
100 | return None
101 | except json.JSONDecodeError:
102 | print("Error: Ethereum wallet file contains invalid JSON.")
103 | return None
104 |
105 | def load_operator_safe_balance(operate_home):
106 | ethereum_json_path = operate_home / "wallets" / "ethereum.json"
107 | try:
108 | with open(ethereum_json_path, "r") as f:
109 | ethereum_data = json.load(f)
110 |
111 | safe_chains = ethereum_data.get("safe_chains", [])
112 | if not safe_chains:
113 | print("Error: Safe chains array is empty.")
114 | return None
115 |
116 | chain_id = safe_chains[0]
117 |
118 | safes = ethereum_data.get("safes", {})
119 | safe_address = safes.get(str(chain_id))
120 | if not safe_address:
121 | print(f"Error: Safe address for chain ID {chain_id} not found in the wallet file.")
122 | return None
123 |
124 | # Here, you should insert your logic to fetch the safe balance from the blockchain
125 | # For illustrative purposes, we'll just return the safe address
126 | return safe_address
127 |
128 | except FileNotFoundError:
129 | print(f"Error: Ethereum wallet file not found at {ethereum_json_path}")
130 | return None
131 | except json.JSONDecodeError:
132 | print("Error: Ethereum wallet file contains invalid JSON.")
133 | return None
134 |
135 | def validate_config(config):
136 | required_keys = ['home_chain_id', 'chain_configs']
137 | for key in required_keys:
138 | if key not in config:
139 | print(f"Error: '{key}' is missing in the configuration.")
140 | return False
141 | return True
142 |
143 | def _get_agent_status() -> str:
144 | operate = OperateApp(
145 | home=OPERATE_HOME,
146 | )
147 | operate.setup()
148 |
149 | optimus_config = load_local_config()
150 | template = get_service_template(optimus_config)
151 | manager = operate.service_manager()
152 | service = get_service(manager, template)
153 | docker_compose_path = service.path / "deployment" / "docker-compose.yaml"
154 | try:
155 | with open(docker_compose_path, "r") as f:
156 | docker_compose = yaml.safe_load(f)
157 |
158 | abci_service_name = None
159 | for service_name in docker_compose["services"]:
160 | if "abci" in service_name:
161 | abci_service_name = service_name
162 | break
163 |
164 | client = docker.from_env()
165 | container = client.containers.get(abci_service_name)
166 | is_running = container.status == "running"
167 | return _color_bool(is_running, "Running", "Stopped")
168 | except FileNotFoundError:
169 | return _color_string("Stopped", ColorCode.RED)
170 | except docker.errors.NotFound:
171 | return _color_string("Not Found", ColorCode.RED)
172 | except docker.errors.DockerException as e:
173 | print(f"Error: Docker exception occurred - {str(e)}")
174 | return _color_string("Error", ColorCode.RED)
175 |
--------------------------------------------------------------------------------
/wallet_info.py:
--------------------------------------------------------------------------------
1 | # wallet_info.py
2 | import json
3 | from pathlib import Path
4 | from web3 import Web3
5 | from web3.middleware import geth_poa_middleware
6 | from decimal import Decimal, getcontext
7 | import logging
8 |
9 | from run_service import (
10 | load_local_config,
11 | get_service_template,
12 | CHAIN_ID_TO_METADATA,
13 | USDC_ADDRESS,
14 | OPERATE_HOME
15 | )
16 |
17 | from utils import (
18 | get_chain_name,
19 | load_operator_address,
20 | validate_config,
21 | wei_to_unit,
22 | wei_to_eth,
23 | ColorCode,
24 | )
25 |
26 | # Set decimal precision
27 | getcontext().prec = 18
28 |
29 | # Configure logging
30 | logging.basicConfig(level=logging.INFO, format='%(message)s')
31 |
32 | TOKEN_ABI = [{
33 | "constant": True,
34 | "inputs": [{"name": "_owner", "type": "address"}],
35 | "name": "balanceOf",
36 | "outputs": [{"name": "balance", "type": "uint256"}],
37 | "type": "function"
38 | }]
39 |
40 | OLAS_ADDRESS = "0xcfD1D50ce23C46D3Cf6407487B2F8934e96DC8f9"
41 |
42 | def load_config():
43 | try:
44 | optimus_config = load_local_config()
45 | service_template = get_service_template(optimus_config)
46 | service_hash = service_template.get("hash")
47 | if not service_hash:
48 | print("Error: Service hash not found in service template.")
49 | return {}
50 | config_path = OPERATE_HOME / f"services/{service_hash}/config.json"
51 | try:
52 | with open(config_path, "r") as f:
53 | return json.load(f)
54 | except FileNotFoundError:
55 | print(f"Error: Config file not found at {config_path}")
56 | return {}
57 | except json.JSONDecodeError:
58 | print("Error: Config file contains invalid JSON.")
59 | return {}
60 | except Exception as e:
61 | print(f"An error occurred while loading the config: {e}")
62 | return {}
63 |
64 | def get_balance(web3, address):
65 | try:
66 | balance = web3.eth.get_balance(address)
67 | return Decimal(Web3.from_wei(balance, 'ether'))
68 | except Exception as e:
69 | print(f"Error getting balance for address {address}: {e}")
70 | return Decimal(0)
71 |
72 | def get_usdc_balance(web3, address, chain_name):
73 | try:
74 | usdc_contract = web3.eth.contract(address=USDC_ADDRESS, abi=TOKEN_ABI)
75 | balance = usdc_contract.functions.balanceOf(address).call()
76 | return Decimal(balance) / Decimal(1e6) # USDC has 6 decimal places
77 | except Exception as e:
78 | print(f"Error getting USDC balance for address {address}: {e}")
79 | return Decimal(0)
80 |
81 | def get_olas_balance(web3, address, chain_name):
82 | try:
83 | olas_contract = web3.eth.contract(address=OLAS_ADDRESS, abi=TOKEN_ABI)
84 | balance = olas_contract.functions.balanceOf(address).call()
85 | return Decimal(balance) / Decimal(1e18) # OLAS has 18 decimal places
86 | except Exception as e:
87 | print(f"Error getting OLAS balance for address {address}: {e}")
88 | return Decimal(0)
89 | class DecimalEncoder(json.JSONEncoder):
90 | def default(self, o):
91 | if isinstance(o, Decimal):
92 | return float(o)
93 | return super(DecimalEncoder, self).default(o)
94 |
95 | def save_wallet_info():
96 | config = load_config()
97 | optimus_config = load_local_config()
98 | if not config:
99 | print("Error: Configuration could not be loaded.")
100 | return
101 |
102 | main_wallet_address = config.get('keys', [{}])[0].get('address')
103 | operator_wallet_address = load_operator_address(OPERATE_HOME)
104 | if not main_wallet_address:
105 | print("Error: Main wallet address not found in configuration.")
106 | return
107 |
108 | main_balances = {}
109 | safe_balances = {}
110 | operator_balances = {}
111 |
112 | for chain_id, chain_config in config.get('chain_configs', {}).items():
113 | chain_name = get_chain_name(chain_id, CHAIN_ID_TO_METADATA)
114 | if optimus_config.allowed_chains and chain_name.lower() not in optimus_config.allowed_chains:
115 | continue
116 |
117 | rpc_url = chain_config.get('ledger_config', {}).get('rpc')
118 | if not rpc_url:
119 | print(f"Error: RPC URL not found for chain ID {chain_id}.")
120 | continue
121 |
122 | try:
123 | web3 = Web3(Web3.HTTPProvider(rpc_url))
124 | if chain_id != "1": # Ethereum Mainnet
125 | web3.middleware_onion.inject(geth_poa_middleware, layer=0)
126 |
127 | # Get main wallet balance
128 | main_balance = get_balance(web3, main_wallet_address)
129 | operator_balance = get_balance(web3, operator_wallet_address)
130 | main_balances[chain_name] = {
131 | "token": "ETH",
132 | "balance": main_balance,
133 | "balance_formatted": f"{main_balance:.6f} ETH"
134 | }
135 | operator_balances[chain_name] = {
136 | "token": "ETH",
137 | "balance": operator_balance,
138 | "balance_formatted": f"{operator_balance:.6f} ETH"
139 | }
140 |
141 | # Get Safe balance
142 | safe_address = chain_config.get('chain_data', {}).get('multisig')
143 | if not safe_address:
144 | print(f"Error: Safe address not found for chain ID {chain_id}.")
145 | continue
146 |
147 | safe_balance = get_balance(web3, safe_address)
148 | safe_balances[chain_name] = {
149 | "address": safe_address,
150 | "token": "ETH",
151 | "balance": safe_balance,
152 | "balance_formatted": f"{safe_balance:.6f} ETH"
153 | }
154 |
155 | # Get USDC balance for Principal Chain
156 | if chain_name.lower() == optimus_config.principal_chain:
157 | usdc_balance = get_usdc_balance(web3, safe_address, chain_name.lower())
158 | safe_balances[chain_name]["usdc_balance"] = usdc_balance
159 | safe_balances[chain_name]["usdc_balance_formatted"] = f"{usdc_balance:.2f} USDC"
160 |
161 | # Get USDC balance for Principal Chain
162 | if chain_name.lower() == optimus_config.staking_chain:
163 | olas_balance = get_olas_balance(web3, safe_address, chain_name.lower())
164 | safe_balances[chain_name]["olas_balance"] = usdc_balance
165 | safe_balances[chain_name]["olas_balance_formatted"] = f"{olas_balance:.6f} OLAS"
166 |
167 | except Exception as e:
168 | print(f"An error occurred while processing chain ID {chain_id}: {e}")
169 | continue
170 |
171 | wallet_info = {
172 | "main_wallet_address": main_wallet_address,
173 | "main_wallet_balances": main_balances,
174 | "operator_wallet_address": operator_wallet_address,
175 | "safe_addresses": {
176 | chain_id: chain_config.get('chain_data', {}).get('multisig', 'N/A')
177 | for chain_id, chain_config in config.get('chain_configs', {}).items()
178 | },
179 | "safe_balances": safe_balances,
180 | "operator_balances": operator_balances,
181 | "chain_configs": {
182 | chain_id: {
183 | "rpc": chain_config.get('ledger_config', {}).get('rpc'),
184 | "multisig": chain_config.get('chain_data', {}).get('multisig'),
185 | "token": chain_config.get('chain_data', {}).get('token'),
186 | "fund_requirements": chain_config.get('chain_data', {}).get('user_params', {}).get('fund_requirements')
187 | }
188 | for chain_id, chain_config in config.get('chain_configs', {}).items()
189 | }
190 | }
191 |
192 | file_path = OPERATE_HOME / "wallets" / "wallet_info.json"
193 | try:
194 | with open(file_path, "w") as f:
195 | json.dump(wallet_info, f, indent=2, cls=DecimalEncoder)
196 | print(f"Wallet info saved to {file_path}")
197 | except Exception as e:
198 | print(f"Error saving wallet info to {file_path}: {e}")
199 |
200 | if __name__ == "__main__":
201 | try:
202 | save_wallet_info()
203 | except Exception as e:
204 | print(f"An unexpected error occurred: {e}")
205 |
--------------------------------------------------------------------------------