olas-operate-middleware 0.10.7__py3-none-any.whl → 0.10.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {olas_operate_middleware-0.10.7.dist-info → olas_operate_middleware-0.10.8.dist-info}/METADATA +1 -1
- {olas_operate_middleware-0.10.7.dist-info → olas_operate_middleware-0.10.8.dist-info}/RECORD +18 -17
- operate/__init__.py +12 -0
- operate/cli.py +145 -23
- operate/constants.py +9 -0
- operate/keys.py +9 -3
- operate/ledger/__init__.py +38 -24
- operate/migration.py +73 -14
- operate/quickstart/reset_password.py +3 -2
- operate/quickstart/run_service.py +7 -2
- operate/services/manage.py +30 -39
- operate/services/protocol.py +7 -9
- operate/services/service.py +40 -70
- operate/wallet/master.py +7 -4
- operate/wallet/wallet_recovery_manager.py +210 -0
- {olas_operate_middleware-0.10.7.dist-info → olas_operate_middleware-0.10.8.dist-info}/LICENSE +0 -0
- {olas_operate_middleware-0.10.7.dist-info → olas_operate_middleware-0.10.8.dist-info}/WHEEL +0 -0
- {olas_operate_middleware-0.10.7.dist-info → olas_operate_middleware-0.10.8.dist-info}/entry_points.txt +0 -0
{olas_operate_middleware-0.10.7.dist-info → olas_operate_middleware-0.10.8.dist-info}/RECORD
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
operate/__init__.py,sha256=
|
|
1
|
+
operate/__init__.py,sha256=PY5BUTF5cmNI-7UwAj2jJlPl6KJ44LoY5e16p7IrE3g,1335
|
|
2
2
|
operate/account/__init__.py,sha256=suJ_vBMO7hLCvLYe3MVDtLXTNDd6P03og7bvUN7fZsE,804
|
|
3
3
|
operate/account/user.py,sha256=y7DqqDpqgHjbVmnfL_cN0Me_JWl3Dh6GSVt2-9FdRVw,3044
|
|
4
4
|
operate/bridge/bridge_manager.py,sha256=sUfhB1pZvBrF-kBfDOfFVoNeesCe7wd4_YahUF3FZDU,17119
|
|
@@ -6,8 +6,8 @@ operate/bridge/providers/lifi_provider.py,sha256=FpAlBAA_gOt-oOHKhGaOQhhTZIL-hgY
|
|
|
6
6
|
operate/bridge/providers/native_bridge_provider.py,sha256=gG8bSyxUoAVEF6_J9tn1qKRv1PnXuMJdMwUAVJ4GJz8,24647
|
|
7
7
|
operate/bridge/providers/provider.py,sha256=i54RL7m4wMSADM_D_V_quQump_ipPTmByUc-c-AOLPQ,19687
|
|
8
8
|
operate/bridge/providers/relay_provider.py,sha256=L7D-PKepsuBadarJmrNneZk0bsrh88u05uD6_2MSss4,17341
|
|
9
|
-
operate/cli.py,sha256=
|
|
10
|
-
operate/constants.py,sha256=
|
|
9
|
+
operate/cli.py,sha256=UZmmQOmod4ytFsMl65MH5rtc881EEE3wQgYIYr-BGW0,55788
|
|
10
|
+
operate/constants.py,sha256=ilmLiv0H_xKZyfqmfEdqMci-TlLQyNPp9rrxxqtkqFI,2992
|
|
11
11
|
operate/data/README.md,sha256=jGPyZTvg2LCGdllvmYxmFMkkkiXb6YWatbqIkcX3kv4,879
|
|
12
12
|
operate/data/__init__.py,sha256=ttC51Yqk9c4ehpIgs1Qbe7aJvzkrbbdZ1ClaCxJYByE,864
|
|
13
13
|
operate/data/contracts/__init__.py,sha256=_th54_WvL0ibGy-b6St0Ne9DX-fyjsh-tNOKDn-cWrg,809
|
|
@@ -57,10 +57,10 @@ operate/data/contracts/uniswap_v2_erc20/contract.py,sha256=MwBks4QmZ3XouMT_TqWLn
|
|
|
57
57
|
operate/data/contracts/uniswap_v2_erc20/contract.yaml,sha256=XUdz-XtKtmZgLfItbO8usP-QPbtUkAxKGn0hL7OftAg,741
|
|
58
58
|
operate/data/contracts/uniswap_v2_erc20/tests/__init__.py,sha256=3Arw8dsCsJz6hVOl0t9UjFASHXbV9yp3hw6x4HqgXpU,847
|
|
59
59
|
operate/data/contracts/uniswap_v2_erc20/tests/test_contract.py,sha256=FzZbw9OTcr_yvjOXpk9YcO-K40eyDARyybcfSHDg2Ps,13392
|
|
60
|
-
operate/keys.py,sha256=
|
|
61
|
-
operate/ledger/__init__.py,sha256=
|
|
60
|
+
operate/keys.py,sha256=_TAS7x9TyU9a8AMnw2FacAHUeqLB0kBSJECc3Msxv54,5207
|
|
61
|
+
operate/ledger/__init__.py,sha256=TCZxTdUTCHjhYErVAjsZgSbv7YUdAzYKOe-vJDvCkGk,3963
|
|
62
62
|
operate/ledger/profiles.py,sha256=7aLLf0pJTIHerpXbwpjeKE9inuwiw0FG0GgTWU_0vGE,11977
|
|
63
|
-
operate/migration.py,sha256=
|
|
63
|
+
operate/migration.py,sha256=mh921s8FG48XMfDu63ojrdhU7j_RZsdl5E2BvxsbkWE,16937
|
|
64
64
|
operate/operate_http/__init__.py,sha256=dxCIVSUos23M4R-PFZZG6k5QrOlEiK0SxhCYSFNxh7U,4711
|
|
65
65
|
operate/operate_http/exceptions.py,sha256=4UFzrn-GyDD71RhkaOyFPBynL6TrrtP3eywaaU3o4fc,1339
|
|
66
66
|
operate/operate_types.py,sha256=oVOzd6K_CTbmAe1pbSFr4LCajNJ_n0y6J5Wwdf_6ev8,8102
|
|
@@ -68,9 +68,9 @@ operate/pearl.py,sha256=yrTpSXLu_ML3qT-uNxq3kScOyo31JyxBujiSMfMUbcg,1690
|
|
|
68
68
|
operate/quickstart/analyse_logs.py,sha256=K11AWWevkddUIUzTe75J3fYVS6aLfi6kT_dAI9OjrX8,4195
|
|
69
69
|
operate/quickstart/claim_staking_rewards.py,sha256=AqfLMRef2YijQtWPaTuGwX2sOItNEkoyoi6Q9SICp_I,4026
|
|
70
70
|
operate/quickstart/reset_configs.py,sha256=ipPpbYyB9gQ4KOKS-xBrRi8fT5LvwctSkQi-8XiUMig,3341
|
|
71
|
-
operate/quickstart/reset_password.py,sha256=
|
|
71
|
+
operate/quickstart/reset_password.py,sha256=78riP7zyFM2JMa1H8Dh6pg-PtLJLQS7PFzx9SECPACQ,2571
|
|
72
72
|
operate/quickstart/reset_staking.py,sha256=SB5LZq9EctG4SYn2M6oPZ7R7ARHSFLRGzAqfKkpRcy0,5111
|
|
73
|
-
operate/quickstart/run_service.py,sha256=
|
|
73
|
+
operate/quickstart/run_service.py,sha256=ruEShJadhIa0E2IsPWu41ObkzZsaWF9XkVH8Vb7uRUU,27146
|
|
74
74
|
operate/quickstart/stop_service.py,sha256=CNcCucI4sqfZG0wTxxh-k14xGcbOh50UGyXqTZVQJP0,2048
|
|
75
75
|
operate/quickstart/terminate_on_chain_service.py,sha256=5ENU8_mkj06i80lKUX-v1QbLU0YzKeOZDUL1e_jzySE,2914
|
|
76
76
|
operate/quickstart/utils.py,sha256=rmd9e7whQIsYpRKqWBEQxMA_SHrivBg6DppFY5ECtQQ,9135
|
|
@@ -79,9 +79,9 @@ operate/services/__init__.py,sha256=isrThS-Ccu5Sc15JZgkN4uTAVaSg-NwUUSDeTyJEqLk,
|
|
|
79
79
|
operate/services/agent_runner.py,sha256=6tJePUJmlRxlIugT2fDaCJHSrQlDnl1t9pbg3-7EmCQ,7560
|
|
80
80
|
operate/services/deployment_runner.py,sha256=Su73o7cdH6fkQfj468K77J04a_TWiokJwbMyVQ25xko,27067
|
|
81
81
|
operate/services/health_checker.py,sha256=2KSEDxG3YmGolUDU--648ny0UJpTAAKvxkcr_VZQv-I,9654
|
|
82
|
-
operate/services/manage.py,sha256=
|
|
83
|
-
operate/services/protocol.py,sha256=
|
|
84
|
-
operate/services/service.py,sha256=
|
|
82
|
+
operate/services/manage.py,sha256=AoJsa4vB3C9cyMLgKBQxsUBv9okyDqxtLjCpsG3UzdQ,115769
|
|
83
|
+
operate/services/protocol.py,sha256=FoJmHz5nukI01-VYtReMsIl3JT5RgZZSLX-mf9EXQ90,62903
|
|
84
|
+
operate/services/service.py,sha256=ZbkTryuK3kBkdvhxd9QDyXRxRFsGGG9WZqbLGOUPloc,39301
|
|
85
85
|
operate/services/utils/__init__.py,sha256=TvioaZ1mfTRUSCtrQoLNAp4WMVXyqEJqFJM4PxSQCRU,24
|
|
86
86
|
operate/services/utils/mech.py,sha256=W2x4dqodivNKXjWU-Brp40QhoUHsIMyNAO7-caMoR0Q,3821
|
|
87
87
|
operate/services/utils/tendermint.py,sha256=3h9nDb2Z89T0RwUr_AaVjqtymQmsu3u6DAVCfL_k1U0,25591
|
|
@@ -89,9 +89,10 @@ operate/utils/__init__.py,sha256=DZNUgg0V9yfNfDrUynp10PErSieJkoxU0AKvsEFIhAw,467
|
|
|
89
89
|
operate/utils/gnosis.py,sha256=CS07ZqvrO7uelkFe09VMyPBcLzKONUI1ZqFvi41BDhc,17924
|
|
90
90
|
operate/utils/ssl.py,sha256=O5DrDoZD4T4qQuHP8GLwWUVxQ-1qXeefGp6uDJiF2lM,4308
|
|
91
91
|
operate/wallet/__init__.py,sha256=NGiozD3XhvkBi7_FaOWQ8x1thZPK4uGpokJaeDY_o2w,813
|
|
92
|
-
operate/wallet/master.py,sha256=
|
|
93
|
-
|
|
94
|
-
olas_operate_middleware-0.10.
|
|
95
|
-
olas_operate_middleware-0.10.
|
|
96
|
-
olas_operate_middleware-0.10.
|
|
97
|
-
olas_operate_middleware-0.10.
|
|
92
|
+
operate/wallet/master.py,sha256=jLFLcRgO6ADok2DigpK59PFJ4mnc_0H9a4RRioYzlvw,30811
|
|
93
|
+
operate/wallet/wallet_recovery_manager.py,sha256=sXEZyvFMePxQKf9NJg4HT90mPg4-7ZcTbvggMnKKzhA,7795
|
|
94
|
+
olas_operate_middleware-0.10.8.dist-info/LICENSE,sha256=mdBDB-mWKV5Cz4ejBzBiKqan6Z8zVLAh9xwM64O2FW4,11339
|
|
95
|
+
olas_operate_middleware-0.10.8.dist-info/METADATA,sha256=1kSqMoV3eXtdFxGckk49yl2kpcubr6PAub2RJ2nZipQ,2035
|
|
96
|
+
olas_operate_middleware-0.10.8.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
97
|
+
olas_operate_middleware-0.10.8.dist-info/entry_points.txt,sha256=dM1g2I7ODApKQFcgl5J4NGA7pfBTo6qsUTXM-j2OLlw,44
|
|
98
|
+
olas_operate_middleware-0.10.8.dist-info/RECORD,,
|
operate/__init__.py
CHANGED
|
@@ -20,6 +20,18 @@
|
|
|
20
20
|
"""Operate app."""
|
|
21
21
|
|
|
22
22
|
import logging
|
|
23
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
23
24
|
|
|
24
25
|
|
|
26
|
+
try:
|
|
27
|
+
# Prefer the distribution name if installed; fall back to the module name
|
|
28
|
+
__version__ = version("olas-operate-middleware")
|
|
29
|
+
except PackageNotFoundError:
|
|
30
|
+
try:
|
|
31
|
+
__version__ = version("operate")
|
|
32
|
+
except PackageNotFoundError:
|
|
33
|
+
logger = logging.getLogger("operate")
|
|
34
|
+
logger.warning("Could not determine version, using 0.0.0+local")
|
|
35
|
+
__version__ = "0.0.0+local"
|
|
36
|
+
|
|
25
37
|
logging.getLogger("aea").setLevel(logging.ERROR)
|
operate/cli.py
CHANGED
|
@@ -20,7 +20,6 @@
|
|
|
20
20
|
"""Operate app CLI module."""
|
|
21
21
|
import asyncio
|
|
22
22
|
import atexit
|
|
23
|
-
import logging
|
|
24
23
|
import multiprocessing
|
|
25
24
|
import os
|
|
26
25
|
import signal
|
|
@@ -46,7 +45,7 @@ from typing_extensions import Annotated
|
|
|
46
45
|
from uvicorn.config import Config
|
|
47
46
|
from uvicorn.server import Server
|
|
48
47
|
|
|
49
|
-
from operate import services
|
|
48
|
+
from operate import __version__, services
|
|
50
49
|
from operate.account.user import UserAccount
|
|
51
50
|
from operate.bridge.bridge_manager import BridgeManager
|
|
52
51
|
from operate.constants import (
|
|
@@ -54,6 +53,9 @@ from operate.constants import (
|
|
|
54
53
|
MIN_PASSWORD_LENGTH,
|
|
55
54
|
OPERATE_HOME,
|
|
56
55
|
SERVICES_DIR,
|
|
56
|
+
USER_JSON,
|
|
57
|
+
WALLETS_DIR,
|
|
58
|
+
WALLET_RECOVERY_DIR,
|
|
57
59
|
ZERO_ADDRESS,
|
|
58
60
|
)
|
|
59
61
|
from operate.ledger.profiles import (
|
|
@@ -76,18 +78,28 @@ from operate.services.health_checker import HealthChecker
|
|
|
76
78
|
from operate.utils import subtract_dicts
|
|
77
79
|
from operate.utils.gnosis import get_assets_balances
|
|
78
80
|
from operate.wallet.master import MasterWalletManager
|
|
81
|
+
from operate.wallet.wallet_recovery_manager import (
|
|
82
|
+
WalletRecoveryError,
|
|
83
|
+
WalletRecoveryManager,
|
|
84
|
+
)
|
|
79
85
|
|
|
80
86
|
|
|
81
87
|
DEFAULT_MAX_RETRIES = 3
|
|
82
88
|
USER_NOT_LOGGED_IN_ERROR = JSONResponse(
|
|
83
89
|
content={"error": "User not logged in."}, status_code=HTTPStatus.UNAUTHORIZED
|
|
84
90
|
)
|
|
91
|
+
USER_LOGGED_IN_ERROR = JSONResponse(
|
|
92
|
+
content={"error": "User must be logged out to perform this operation."},
|
|
93
|
+
status_code=HTTPStatus.FORBIDDEN,
|
|
94
|
+
)
|
|
85
95
|
ACCOUNT_NOT_FOUND_ERROR = JSONResponse(
|
|
86
96
|
content={"error": "User account not found."},
|
|
87
97
|
status_code=HTTPStatus.NOT_FOUND,
|
|
88
98
|
)
|
|
89
99
|
TRY_TO_SHUTDOWN_PREVIOUS_INSTANCE = True
|
|
90
100
|
|
|
101
|
+
logger = setup_logger(name="operate")
|
|
102
|
+
|
|
91
103
|
|
|
92
104
|
def service_not_found_error(service_config_id: str) -> JSONResponse:
|
|
93
105
|
"""Service not found error response"""
|
|
@@ -103,7 +115,6 @@ class OperateApp:
|
|
|
103
115
|
def __init__(
|
|
104
116
|
self,
|
|
105
117
|
home: t.Optional[Path] = None,
|
|
106
|
-
logger: t.Optional[logging.Logger] = None,
|
|
107
118
|
) -> None:
|
|
108
119
|
"""Initialize object."""
|
|
109
120
|
super().__init__()
|
|
@@ -112,14 +123,13 @@ class OperateApp:
|
|
|
112
123
|
self._keys = self._path / KEYS_DIR
|
|
113
124
|
self.setup()
|
|
114
125
|
|
|
115
|
-
self.logger = logger or setup_logger(name="operate")
|
|
116
126
|
services.manage.KeysManager(
|
|
117
127
|
path=self._keys,
|
|
118
|
-
logger=
|
|
128
|
+
logger=logger,
|
|
119
129
|
)
|
|
120
130
|
self.password: t.Optional[str] = os.environ.get("OPERATE_USER_PASSWORD")
|
|
121
131
|
|
|
122
|
-
mm = MigrationManager(self._path,
|
|
132
|
+
mm = MigrationManager(self._path, logger)
|
|
123
133
|
mm.migrate_user_account()
|
|
124
134
|
mm.migrate_services(self.service_manager())
|
|
125
135
|
mm.migrate_wallets(self.wallet_manager)
|
|
@@ -130,14 +140,14 @@ class OperateApp:
|
|
|
130
140
|
self.password = password
|
|
131
141
|
return UserAccount.new(
|
|
132
142
|
password=password,
|
|
133
|
-
path=self._path /
|
|
143
|
+
path=self._path / USER_JSON,
|
|
134
144
|
)
|
|
135
145
|
|
|
136
146
|
def update_password(self, old_password: str, new_password: str) -> None:
|
|
137
147
|
"""Updates current password"""
|
|
138
148
|
|
|
139
149
|
if not new_password:
|
|
140
|
-
raise ValueError("'
|
|
150
|
+
raise ValueError("'new_password' is required.")
|
|
141
151
|
|
|
142
152
|
if not (
|
|
143
153
|
self.user_account.is_valid(old_password)
|
|
@@ -154,7 +164,7 @@ class OperateApp:
|
|
|
154
164
|
"""Updates current password using the mnemonic"""
|
|
155
165
|
|
|
156
166
|
if not new_password:
|
|
157
|
-
raise ValueError("'
|
|
167
|
+
raise ValueError("'new_password' is required.")
|
|
158
168
|
|
|
159
169
|
mnemonic = mnemonic.strip().lower()
|
|
160
170
|
if not self.wallet_manager.is_mnemonic_valid(mnemonic):
|
|
@@ -171,37 +181,45 @@ class OperateApp:
|
|
|
171
181
|
return services.manage.ServiceManager(
|
|
172
182
|
path=self._services,
|
|
173
183
|
wallet_manager=self.wallet_manager,
|
|
174
|
-
logger=
|
|
184
|
+
logger=logger,
|
|
175
185
|
skip_dependency_check=skip_dependency_check,
|
|
176
186
|
)
|
|
177
187
|
|
|
178
188
|
@property
|
|
179
189
|
def user_account(self) -> t.Optional[UserAccount]:
|
|
180
190
|
"""Load user account."""
|
|
181
|
-
|
|
182
|
-
UserAccount.load(self._path /
|
|
183
|
-
|
|
184
|
-
else None
|
|
185
|
-
)
|
|
191
|
+
if (self._path / USER_JSON).exists():
|
|
192
|
+
return UserAccount.load(self._path / USER_JSON)
|
|
193
|
+
return None
|
|
186
194
|
|
|
187
195
|
@property
|
|
188
196
|
def wallet_manager(self) -> MasterWalletManager:
|
|
189
|
-
"""Load
|
|
197
|
+
"""Load wallet manager."""
|
|
190
198
|
manager = MasterWalletManager(
|
|
191
|
-
path=self._path /
|
|
199
|
+
path=self._path / WALLETS_DIR,
|
|
192
200
|
password=self.password,
|
|
193
|
-
logger=
|
|
201
|
+
logger=logger,
|
|
194
202
|
)
|
|
195
203
|
manager.setup()
|
|
196
204
|
return manager
|
|
197
205
|
|
|
206
|
+
@property
|
|
207
|
+
def wallet_recoverey_manager(self) -> WalletRecoveryManager:
|
|
208
|
+
"""Load wallet recovery manager."""
|
|
209
|
+
manager = WalletRecoveryManager(
|
|
210
|
+
path=self._path / WALLET_RECOVERY_DIR,
|
|
211
|
+
wallet_manager=self.wallet_manager,
|
|
212
|
+
logger=self.logger,
|
|
213
|
+
)
|
|
214
|
+
return manager
|
|
215
|
+
|
|
198
216
|
@property
|
|
199
217
|
def bridge_manager(self) -> BridgeManager:
|
|
200
|
-
"""Load
|
|
218
|
+
"""Load bridge manager."""
|
|
201
219
|
manager = BridgeManager(
|
|
202
220
|
path=self._path / "bridge",
|
|
203
221
|
wallet_manager=self.wallet_manager,
|
|
204
|
-
logger=
|
|
222
|
+
logger=logger,
|
|
205
223
|
)
|
|
206
224
|
return manager
|
|
207
225
|
|
|
@@ -232,10 +250,9 @@ def create_app( # pylint: disable=too-many-locals, unused-argument, too-many-st
|
|
|
232
250
|
)
|
|
233
251
|
)
|
|
234
252
|
|
|
235
|
-
logger = setup_logger(name="operate")
|
|
236
253
|
if HEALTH_CHECKER_OFF:
|
|
237
254
|
logger.warning("Healthchecker is off!!!")
|
|
238
|
-
operate = OperateApp(home=home
|
|
255
|
+
operate = OperateApp(home=home)
|
|
239
256
|
|
|
240
257
|
funding_jobs: t.Dict[str, asyncio.Task] = {}
|
|
241
258
|
health_checker = HealthChecker(
|
|
@@ -877,6 +894,21 @@ def create_app( # pylint: disable=too-many-locals, unused-argument, too-many-st
|
|
|
877
894
|
deployment_json["healthcheck"] = service.get_latest_healthcheck()
|
|
878
895
|
return JSONResponse(content=deployment_json)
|
|
879
896
|
|
|
897
|
+
@app.get("/api/v2/service/{service_config_id}/agent_performance")
|
|
898
|
+
@with_retries
|
|
899
|
+
async def _get_agent_performance(request: Request) -> JSONResponse:
|
|
900
|
+
"""Get the service refill requirements."""
|
|
901
|
+
service_config_id = request.path_params["service_config_id"]
|
|
902
|
+
|
|
903
|
+
if not operate.service_manager().exists(service_config_id=service_config_id):
|
|
904
|
+
return service_not_found_error(service_config_id=service_config_id)
|
|
905
|
+
|
|
906
|
+
return JSONResponse(
|
|
907
|
+
content=operate.service_manager()
|
|
908
|
+
.load(service_config_id=service_config_id)
|
|
909
|
+
.get_agent_performance()
|
|
910
|
+
)
|
|
911
|
+
|
|
880
912
|
@app.get("/api/v2/service/{service_config_id}/refill_requirements")
|
|
881
913
|
@with_retries
|
|
882
914
|
async def _get_refill_requirements(request: Request) -> JSONResponse:
|
|
@@ -1166,12 +1198,103 @@ def create_app( # pylint: disable=too-many-locals, unused-argument, too-many-st
|
|
|
1166
1198
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
1167
1199
|
)
|
|
1168
1200
|
|
|
1201
|
+
@app.post("/api/wallet/recovery/initiate")
|
|
1202
|
+
@with_retries
|
|
1203
|
+
async def _wallet_recovery_initiate(request: Request) -> JSONResponse:
|
|
1204
|
+
"""Initiate wallet recovery."""
|
|
1205
|
+
if operate.user_account is None:
|
|
1206
|
+
return ACCOUNT_NOT_FOUND_ERROR
|
|
1207
|
+
|
|
1208
|
+
if operate.password:
|
|
1209
|
+
return USER_LOGGED_IN_ERROR
|
|
1210
|
+
|
|
1211
|
+
data = await request.json()
|
|
1212
|
+
new_password = data.get("new_password")
|
|
1213
|
+
|
|
1214
|
+
if not new_password or len(new_password) < MIN_PASSWORD_LENGTH:
|
|
1215
|
+
return JSONResponse(
|
|
1216
|
+
content={
|
|
1217
|
+
"error": f"New password must be at least {MIN_PASSWORD_LENGTH} characters long."
|
|
1218
|
+
},
|
|
1219
|
+
status_code=HTTPStatus.BAD_REQUEST,
|
|
1220
|
+
)
|
|
1221
|
+
|
|
1222
|
+
try:
|
|
1223
|
+
output = operate.wallet_recoverey_manager.initiate_recovery(
|
|
1224
|
+
new_password=new_password
|
|
1225
|
+
)
|
|
1226
|
+
return JSONResponse(
|
|
1227
|
+
content=output,
|
|
1228
|
+
status_code=HTTPStatus.OK,
|
|
1229
|
+
)
|
|
1230
|
+
except (ValueError, WalletRecoveryError) as e:
|
|
1231
|
+
logger.error(f"_recovery_initiate error: {e}")
|
|
1232
|
+
return JSONResponse(
|
|
1233
|
+
content={"error": f"Failed to initiate recovery: {e}"},
|
|
1234
|
+
status_code=HTTPStatus.BAD_REQUEST,
|
|
1235
|
+
)
|
|
1236
|
+
except Exception as e: # pylint: disable=broad-except
|
|
1237
|
+
logger.error(f"_recovery_initiate error: {e}\n{traceback.format_exc()}")
|
|
1238
|
+
return JSONResponse(
|
|
1239
|
+
content={
|
|
1240
|
+
"error": "Failed to initiate recovery. Please check the logs."
|
|
1241
|
+
},
|
|
1242
|
+
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
1243
|
+
)
|
|
1244
|
+
|
|
1245
|
+
@app.post("/api/wallet/recovery/complete")
|
|
1246
|
+
@with_retries
|
|
1247
|
+
async def _wallet_recovery_complete(request: Request) -> JSONResponse:
|
|
1248
|
+
"""Complete wallet recovery."""
|
|
1249
|
+
if operate.user_account is None:
|
|
1250
|
+
return ACCOUNT_NOT_FOUND_ERROR
|
|
1251
|
+
|
|
1252
|
+
if operate.password:
|
|
1253
|
+
return USER_LOGGED_IN_ERROR
|
|
1254
|
+
|
|
1255
|
+
data = await request.json()
|
|
1256
|
+
bundle_id = data.get("id")
|
|
1257
|
+
password = data.get("password")
|
|
1258
|
+
raise_if_inconsistent_owners = data.get("require_consistent_owners", True)
|
|
1259
|
+
|
|
1260
|
+
try:
|
|
1261
|
+
operate.wallet_recoverey_manager.complete_recovery(
|
|
1262
|
+
bundle_id=bundle_id,
|
|
1263
|
+
password=password,
|
|
1264
|
+
raise_if_inconsistent_owners=raise_if_inconsistent_owners,
|
|
1265
|
+
)
|
|
1266
|
+
return JSONResponse(
|
|
1267
|
+
content=operate.wallet_manager.json,
|
|
1268
|
+
status_code=HTTPStatus.OK,
|
|
1269
|
+
)
|
|
1270
|
+
except KeyError as e:
|
|
1271
|
+
logger.error(f"_recovery_complete error: {e}")
|
|
1272
|
+
return JSONResponse(
|
|
1273
|
+
content={"error": f"Failed to complete recovery: {e}"},
|
|
1274
|
+
status_code=HTTPStatus.NOT_FOUND,
|
|
1275
|
+
)
|
|
1276
|
+
except (ValueError, WalletRecoveryError) as e:
|
|
1277
|
+
logger.error(f"_recovery_complete error: {e}")
|
|
1278
|
+
return JSONResponse(
|
|
1279
|
+
content={"error": f"Failed to complete recovery: {e}"},
|
|
1280
|
+
status_code=HTTPStatus.BAD_REQUEST,
|
|
1281
|
+
)
|
|
1282
|
+
except Exception as e: # pylint: disable=broad-except
|
|
1283
|
+
logger.error(f"_recovery_complete error: {e}\n{traceback.format_exc()}")
|
|
1284
|
+
return JSONResponse(
|
|
1285
|
+
content={
|
|
1286
|
+
"error": "Failed to complete recovery. Please check the logs."
|
|
1287
|
+
},
|
|
1288
|
+
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
1289
|
+
)
|
|
1290
|
+
|
|
1169
1291
|
return app
|
|
1170
1292
|
|
|
1171
1293
|
|
|
1172
1294
|
@group(name="operate")
|
|
1173
1295
|
def _operate() -> None:
|
|
1174
1296
|
"""Operate - deploy autonomous services."""
|
|
1297
|
+
logger.info(f"Operate version: {__version__}")
|
|
1175
1298
|
|
|
1176
1299
|
|
|
1177
1300
|
@_operate.command(name="daemon")
|
|
@@ -1188,7 +1311,6 @@ def _daemon(
|
|
|
1188
1311
|
) -> None:
|
|
1189
1312
|
"""Launch operate daemon."""
|
|
1190
1313
|
app = create_app(home=home)
|
|
1191
|
-
logger = setup_logger(name="daemon")
|
|
1192
1314
|
|
|
1193
1315
|
config_kwargs = {
|
|
1194
1316
|
"app": app,
|
operate/constants.py
CHANGED
|
@@ -26,9 +26,18 @@ OPERATE = ".operate"
|
|
|
26
26
|
OPERATE_HOME = Path.cwd() / OPERATE
|
|
27
27
|
SERVICES_DIR = "services"
|
|
28
28
|
KEYS_DIR = "keys"
|
|
29
|
+
WALLETS_DIR = "wallets"
|
|
30
|
+
WALLET_RECOVERY_DIR = "wallet_recovery"
|
|
29
31
|
DEPLOYMENT_DIR = "deployment"
|
|
30
32
|
DEPLOYMENT_JSON = "deployment.json"
|
|
31
33
|
CONFIG_JSON = "config.json"
|
|
34
|
+
USER_JSON = "user.json"
|
|
35
|
+
|
|
36
|
+
AGENT_PERSISTENT_STORAGE_DIR = "persistent_data"
|
|
37
|
+
AGENT_PERSISTENT_STORAGE_ENV_VAR = "STORE_PATH"
|
|
38
|
+
AGENT_LOG_DIR = "benchmarks"
|
|
39
|
+
AGENT_LOG_ENV_VAR = "LOG_DIR"
|
|
40
|
+
|
|
32
41
|
ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"
|
|
33
42
|
|
|
34
43
|
ON_CHAIN_INTERACT_TIMEOUT = 120.0
|
operate/keys.py
CHANGED
|
@@ -92,16 +92,22 @@ class KeysManager(metaclass=SingletonMeta):
|
|
|
92
92
|
suffix=".txt",
|
|
93
93
|
delete=False, # Handle cleanup manually
|
|
94
94
|
) as temp_file:
|
|
95
|
+
temp_file_name = temp_file.name
|
|
95
96
|
temp_file.write(key.private_key)
|
|
96
97
|
temp_file.flush()
|
|
97
98
|
temp_file.close() # Close the file before reading
|
|
98
99
|
|
|
99
100
|
# Set proper file permissions (readable by owner only)
|
|
100
|
-
os.chmod(
|
|
101
|
-
crypto = EthereumCrypto(private_key_path=
|
|
101
|
+
os.chmod(temp_file_name, 0o600)
|
|
102
|
+
crypto = EthereumCrypto(private_key_path=temp_file_name)
|
|
102
103
|
|
|
103
104
|
try:
|
|
104
|
-
|
|
105
|
+
with open(temp_file_name, "r+", encoding="utf-8") as f:
|
|
106
|
+
f.seek(0)
|
|
107
|
+
f.write("\0" * len(key.private_key))
|
|
108
|
+
f.flush()
|
|
109
|
+
f.close()
|
|
110
|
+
os.unlink(temp_file_name) # Clean up the temporary file
|
|
105
111
|
except OSError as e:
|
|
106
112
|
self.logger.error(f"Failed to delete temp file {temp_file.name}: {e}")
|
|
107
113
|
|
operate/ledger/__init__.py
CHANGED
|
@@ -24,62 +24,76 @@ import os
|
|
|
24
24
|
from operate.operate_types import Chain
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
ARBITRUM_ONE_PUBLIC_RPC = os.environ.get(
|
|
28
|
+
"ARBITRUM_ONE_RPC", "https://arb1.arbitrum.io/rpc"
|
|
29
|
+
)
|
|
30
30
|
BASE_PUBLIC_RPC = os.environ.get("BASE_RPC", "https://mainnet.base.org")
|
|
31
31
|
CELO_PUBLIC_RPC = os.environ.get("CELO_RPC", "https://forno.celo.org")
|
|
32
|
-
|
|
32
|
+
ETHEREUM_PUBLIC_RPC = os.environ.get("ETHEREUM_RPC", "https://ethereum.publicnode.com")
|
|
33
|
+
GNOSIS_PUBLIC_RPC = os.environ.get("GNOSIS_RPC", "https://gnosis-rpc.publicnode.com")
|
|
33
34
|
MODE_PUBLIC_RPC = os.environ.get("MODE_RPC", "https://mainnet.mode.network/")
|
|
35
|
+
OPTIMISM_PUBLIC_RPC = os.environ.get("OPTIMISM_RPC", "https://mainnet.optimism.io")
|
|
36
|
+
POLYGON_PUBLIC_RPC = os.environ.get("POLYGON_RPC", "https://polygon-rpc.com")
|
|
37
|
+
SOLANA_PUBLIC_RPC = os.environ.get("SOLANA_RPC", "https://api.mainnet-beta.solana.com")
|
|
34
38
|
|
|
35
|
-
|
|
36
|
-
GNOSIS_RPC = os.environ.get("GNOSIS_RPC", "https://rpc-gate.autonolas.tech/gnosis-rpc/")
|
|
37
|
-
SOLANA_RPC = os.environ.get("SOLANA_RPC", "https://api.mainnet-beta.solana.com")
|
|
39
|
+
ARBITRUM_ONE_RPC = os.environ.get("ARBITRUM_ONE_RPC", "https://arb1.arbitrum.io/rpc")
|
|
38
40
|
BASE_RPC = os.environ.get("BASE_RPC", "https://mainnet.base.org")
|
|
39
41
|
CELO_RPC = os.environ.get("CELO_RPC", "https://forno.celo.org")
|
|
40
|
-
|
|
42
|
+
ETHEREUM_RPC = os.environ.get("ETHEREUM_RPC", "https://ethereum.publicnode.com")
|
|
43
|
+
GNOSIS_RPC = os.environ.get("GNOSIS_RPC", "https://rpc-gate.autonolas.tech/gnosis-rpc/")
|
|
41
44
|
MODE_RPC = os.environ.get("MODE_RPC", "https://mainnet.mode.network/")
|
|
45
|
+
OPTIMISM_RPC = os.environ.get("OPTIMISM_RPC", "https://mainnet.optimism.io")
|
|
46
|
+
POLYGON_RPC = os.environ.get("POLYGON_RPC", "https://polygon-rpc.com")
|
|
47
|
+
SOLANA_RPC = os.environ.get("SOLANA_RPC", "https://api.mainnet-beta.solana.com")
|
|
42
48
|
|
|
43
49
|
PUBLIC_RPCS = {
|
|
44
|
-
Chain.
|
|
45
|
-
Chain.GNOSIS: GNOSIS_PUBLIC_RPC,
|
|
46
|
-
Chain.SOLANA: SOLANA_PUBLIC_RPC,
|
|
50
|
+
Chain.ARBITRUM_ONE: ARBITRUM_ONE_PUBLIC_RPC,
|
|
47
51
|
Chain.BASE: BASE_PUBLIC_RPC,
|
|
48
52
|
Chain.CELO: CELO_PUBLIC_RPC,
|
|
49
|
-
Chain.
|
|
53
|
+
Chain.ETHEREUM: ETHEREUM_PUBLIC_RPC,
|
|
54
|
+
Chain.GNOSIS: GNOSIS_PUBLIC_RPC,
|
|
50
55
|
Chain.MODE: MODE_PUBLIC_RPC,
|
|
56
|
+
Chain.OPTIMISM: OPTIMISM_PUBLIC_RPC,
|
|
57
|
+
Chain.POLYGON: POLYGON_PUBLIC_RPC,
|
|
58
|
+
Chain.SOLANA: SOLANA_PUBLIC_RPC,
|
|
51
59
|
}
|
|
52
60
|
|
|
53
61
|
DEFAULT_RPCS = {
|
|
54
|
-
Chain.
|
|
55
|
-
Chain.GNOSIS: GNOSIS_RPC,
|
|
56
|
-
Chain.SOLANA: SOLANA_RPC,
|
|
62
|
+
Chain.ARBITRUM_ONE: ARBITRUM_ONE_RPC,
|
|
57
63
|
Chain.BASE: BASE_RPC,
|
|
58
64
|
Chain.CELO: CELO_RPC,
|
|
59
|
-
Chain.
|
|
65
|
+
Chain.ETHEREUM: ETHEREUM_RPC,
|
|
66
|
+
Chain.GNOSIS: GNOSIS_RPC,
|
|
60
67
|
Chain.MODE: MODE_RPC,
|
|
68
|
+
Chain.OPTIMISM: OPTIMISM_RPC,
|
|
69
|
+
Chain.POLYGON: POLYGON_RPC,
|
|
70
|
+
Chain.SOLANA: SOLANA_RPC,
|
|
61
71
|
}
|
|
62
72
|
|
|
63
73
|
# Base currency for each chain
|
|
64
74
|
CURRENCY_DENOMS = {
|
|
65
|
-
Chain.
|
|
66
|
-
Chain.GNOSIS: "xDAI",
|
|
67
|
-
Chain.SOLANA: "SOL",
|
|
75
|
+
Chain.ARBITRUM_ONE: "ETH",
|
|
68
76
|
Chain.BASE: "ETH",
|
|
69
77
|
Chain.CELO: "CELO",
|
|
70
|
-
Chain.
|
|
78
|
+
Chain.ETHEREUM: "ETH",
|
|
79
|
+
Chain.GNOSIS: "xDAI",
|
|
71
80
|
Chain.MODE: "ETH",
|
|
81
|
+
Chain.OPTIMISM: "ETH",
|
|
82
|
+
Chain.POLYGON: "POL",
|
|
83
|
+
Chain.SOLANA: "SOL",
|
|
72
84
|
}
|
|
73
85
|
|
|
74
86
|
# Smallest denomination for each chain
|
|
75
87
|
CURRENCY_SMALLEST_UNITS = {
|
|
76
|
-
Chain.
|
|
77
|
-
Chain.GNOSIS: "Wei",
|
|
78
|
-
Chain.SOLANA: "Lamport",
|
|
88
|
+
Chain.ARBITRUM_ONE: "Wei",
|
|
79
89
|
Chain.BASE: "Wei",
|
|
80
90
|
Chain.CELO: "Wei",
|
|
81
|
-
Chain.
|
|
91
|
+
Chain.ETHEREUM: "Wei",
|
|
92
|
+
Chain.GNOSIS: "Wei",
|
|
82
93
|
Chain.MODE: "Wei",
|
|
94
|
+
Chain.OPTIMISM: "Wei",
|
|
95
|
+
Chain.POLYGON: "Wei",
|
|
96
|
+
Chain.SOLANA: "Lamport",
|
|
83
97
|
}
|
|
84
98
|
|
|
85
99
|
|
operate/migration.py
CHANGED
|
@@ -29,11 +29,10 @@ from time import time
|
|
|
29
29
|
|
|
30
30
|
from aea_cli_ipfs.ipfs_utils import IPFSTool
|
|
31
31
|
|
|
32
|
-
from operate.constants import ZERO_ADDRESS
|
|
32
|
+
from operate.constants import USER_JSON, ZERO_ADDRESS
|
|
33
33
|
from operate.operate_types import Chain, LedgerType
|
|
34
34
|
from operate.services.manage import ServiceManager
|
|
35
35
|
from operate.services.service import (
|
|
36
|
-
DEFAULT_TRADER_ENV_VARS,
|
|
37
36
|
NON_EXISTENT_MULTISIG,
|
|
38
37
|
SERVICE_CONFIG_PREFIX,
|
|
39
38
|
SERVICE_CONFIG_VERSION,
|
|
@@ -43,11 +42,67 @@ from operate.utils import create_backup
|
|
|
43
42
|
from operate.wallet.master import LEDGER_TYPE_TO_WALLET_CLASS, MasterWalletManager
|
|
44
43
|
|
|
45
44
|
|
|
45
|
+
DEFAULT_TRADER_ENV_VARS = {
|
|
46
|
+
"GNOSIS_LEDGER_RPC": {
|
|
47
|
+
"name": "Gnosis ledger RPC",
|
|
48
|
+
"description": "",
|
|
49
|
+
"value": "",
|
|
50
|
+
"provision_type": "computed",
|
|
51
|
+
},
|
|
52
|
+
"STAKING_CONTRACT_ADDRESS": {
|
|
53
|
+
"name": "Staking contract address",
|
|
54
|
+
"description": "",
|
|
55
|
+
"value": "",
|
|
56
|
+
"provision_type": "computed",
|
|
57
|
+
},
|
|
58
|
+
"MECH_MARKETPLACE_CONFIG": {
|
|
59
|
+
"name": "Mech marketplace configuration",
|
|
60
|
+
"description": "",
|
|
61
|
+
"value": "",
|
|
62
|
+
"provision_type": "computed",
|
|
63
|
+
},
|
|
64
|
+
"MECH_ACTIVITY_CHECKER_CONTRACT": {
|
|
65
|
+
"name": "Mech activity checker contract",
|
|
66
|
+
"description": "",
|
|
67
|
+
"value": "",
|
|
68
|
+
"provision_type": "computed",
|
|
69
|
+
},
|
|
70
|
+
"MECH_CONTRACT_ADDRESS": {
|
|
71
|
+
"name": "Mech contract address",
|
|
72
|
+
"description": "",
|
|
73
|
+
"value": "",
|
|
74
|
+
"provision_type": "computed",
|
|
75
|
+
},
|
|
76
|
+
"MECH_REQUEST_PRICE": {
|
|
77
|
+
"name": "Mech request price",
|
|
78
|
+
"description": "",
|
|
79
|
+
"value": "10000000000000000",
|
|
80
|
+
"provision_type": "computed",
|
|
81
|
+
},
|
|
82
|
+
"USE_MECH_MARKETPLACE": {
|
|
83
|
+
"name": "Use Mech marketplace",
|
|
84
|
+
"description": "",
|
|
85
|
+
"value": "False",
|
|
86
|
+
"provision_type": "computed",
|
|
87
|
+
},
|
|
88
|
+
"REQUESTER_STAKING_INSTANCE_ADDRESS": {
|
|
89
|
+
"name": "Requester staking instance address",
|
|
90
|
+
"description": "",
|
|
91
|
+
"value": "",
|
|
92
|
+
"provision_type": "computed",
|
|
93
|
+
},
|
|
94
|
+
"PRIORITY_MECH_ADDRESS": {
|
|
95
|
+
"name": "Priority Mech address",
|
|
96
|
+
"description": "",
|
|
97
|
+
"value": "",
|
|
98
|
+
"provision_type": "computed",
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
46
103
|
class MigrationManager:
|
|
47
104
|
"""MigrationManager"""
|
|
48
105
|
|
|
49
|
-
# TODO Backport here migration for services/config.json, etc.
|
|
50
|
-
|
|
51
106
|
def __init__(
|
|
52
107
|
self,
|
|
53
108
|
home: Path,
|
|
@@ -62,12 +117,12 @@ class MigrationManager:
|
|
|
62
117
|
"""Log directories present in `path`."""
|
|
63
118
|
directories = [f" - {str(p)}" for p in path.iterdir() if p.is_dir()]
|
|
64
119
|
directories_str = "\n".join(directories)
|
|
65
|
-
self.logger.info(f"Directories in {path}
|
|
120
|
+
self.logger.info(f"Directories in {path}:\n{directories_str}")
|
|
66
121
|
|
|
67
122
|
def migrate_user_account(self) -> None:
|
|
68
123
|
"""Migrates user.json"""
|
|
69
124
|
|
|
70
|
-
path = self._path /
|
|
125
|
+
path = self._path / USER_JSON
|
|
71
126
|
if not path.exists():
|
|
72
127
|
return
|
|
73
128
|
|
|
@@ -102,18 +157,22 @@ class MigrationManager:
|
|
|
102
157
|
|
|
103
158
|
self.logger.info("Migrating wallet configs done.")
|
|
104
159
|
|
|
105
|
-
@staticmethod
|
|
106
160
|
def _migrate_service( # pylint: disable=too-many-statements,too-many-locals
|
|
161
|
+
self,
|
|
107
162
|
path: Path,
|
|
108
163
|
) -> bool:
|
|
109
164
|
"""Migrate the JSON file format if needed."""
|
|
110
165
|
|
|
111
166
|
if not path.is_dir():
|
|
167
|
+
self.logger.warning(f"Service config path {path} is not a directory.")
|
|
112
168
|
return False
|
|
113
169
|
|
|
114
170
|
if not path.name.startswith(SERVICE_CONFIG_PREFIX) and not path.name.startswith(
|
|
115
171
|
"bafybei"
|
|
116
172
|
):
|
|
173
|
+
self.logger.warning(
|
|
174
|
+
f"Service config path {path} is not a valid service config."
|
|
175
|
+
)
|
|
117
176
|
return False
|
|
118
177
|
|
|
119
178
|
if path.name.startswith("bafybei"):
|
|
@@ -155,6 +214,10 @@ class MigrationManager:
|
|
|
155
214
|
if version == SERVICE_CONFIG_VERSION:
|
|
156
215
|
return False
|
|
157
216
|
|
|
217
|
+
self.logger.info(
|
|
218
|
+
f"Migrating service config in {path} from version {version} to {SERVICE_CONFIG_VERSION}..."
|
|
219
|
+
)
|
|
220
|
+
|
|
158
221
|
# Migration steps for older versions
|
|
159
222
|
if version == 0:
|
|
160
223
|
new_data = {
|
|
@@ -344,13 +407,9 @@ class MigrationManager:
|
|
|
344
407
|
paths = list(service_manager.path.iterdir())
|
|
345
408
|
for path in paths:
|
|
346
409
|
try:
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
self.logger.info(f"migrate_service_configs {str(path)}")
|
|
351
|
-
migrated = self._migrate_service(path)
|
|
352
|
-
if migrated:
|
|
353
|
-
self.logger.info(f"Folder {str(path)} has been migrated.")
|
|
410
|
+
migrated = self._migrate_service(path)
|
|
411
|
+
if migrated:
|
|
412
|
+
self.logger.info(f"Folder {str(path)} has been migrated.")
|
|
354
413
|
except Exception as e: # pylint: disable=broad-except
|
|
355
414
|
self.logger.error(
|
|
356
415
|
f"Failed to migrate service: {path.name}. Exception {e}: {traceback.format_exc()}"
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
from typing import TYPE_CHECKING
|
|
22
22
|
|
|
23
23
|
from operate.account.user import UserAccount
|
|
24
|
+
from operate.constants import USER_JSON
|
|
24
25
|
from operate.operate_types import LedgerType
|
|
25
26
|
from operate.quickstart.run_service import ask_confirm_password
|
|
26
27
|
from operate.quickstart.utils import ask_or_get_from_env, print_section, print_title
|
|
@@ -36,7 +37,7 @@ def reset_password(operate: "OperateApp") -> None:
|
|
|
36
37
|
print_title("Reset your password")
|
|
37
38
|
|
|
38
39
|
# check if agent was started before
|
|
39
|
-
if not (operate._path /
|
|
40
|
+
if not (operate._path / USER_JSON).exists():
|
|
40
41
|
print("No previous agent setup found. Exiting.")
|
|
41
42
|
return
|
|
42
43
|
|
|
@@ -57,7 +58,7 @@ def reset_password(operate: "OperateApp") -> None:
|
|
|
57
58
|
print("Resetting password of user account...")
|
|
58
59
|
UserAccount.new(
|
|
59
60
|
password=old_password,
|
|
60
|
-
path=operate._path /
|
|
61
|
+
path=operate._path / USER_JSON,
|
|
61
62
|
).update(
|
|
62
63
|
old_password=old_password,
|
|
63
64
|
new_password=new_password,
|
|
@@ -34,7 +34,12 @@ from halo import Halo # type: ignore[import]
|
|
|
34
34
|
from web3.exceptions import Web3Exception
|
|
35
35
|
|
|
36
36
|
from operate.account.user import UserAccount
|
|
37
|
-
from operate.constants import
|
|
37
|
+
from operate.constants import (
|
|
38
|
+
IPFS_ADDRESS,
|
|
39
|
+
NO_STAKING_PROGRAM_ID,
|
|
40
|
+
OPERATE_HOME,
|
|
41
|
+
USER_JSON,
|
|
42
|
+
)
|
|
38
43
|
from operate.data import DATA_DIR
|
|
39
44
|
from operate.data.contracts.staking_token.contract import StakingTokenContract
|
|
40
45
|
from operate.ledger.profiles import STAKING, get_staking_contract
|
|
@@ -453,7 +458,7 @@ def ask_password_if_needed(operate: "OperateApp") -> None:
|
|
|
453
458
|
password = ask_confirm_password()
|
|
454
459
|
UserAccount.new(
|
|
455
460
|
password=password,
|
|
456
|
-
path=operate._path /
|
|
461
|
+
path=operate._path / USER_JSON,
|
|
457
462
|
)
|
|
458
463
|
else:
|
|
459
464
|
_password = None
|
operate/services/manage.py
CHANGED
|
@@ -23,7 +23,6 @@ import asyncio
|
|
|
23
23
|
import json
|
|
24
24
|
import logging
|
|
25
25
|
import os
|
|
26
|
-
import tempfile
|
|
27
26
|
import traceback
|
|
28
27
|
import typing as t
|
|
29
28
|
from collections import Counter, defaultdict
|
|
@@ -34,13 +33,20 @@ from pathlib import Path
|
|
|
34
33
|
|
|
35
34
|
import requests
|
|
36
35
|
from aea.helpers.base import IPFSHash
|
|
37
|
-
from aea_ledger_ethereum import
|
|
36
|
+
from aea_ledger_ethereum import LedgerApi
|
|
38
37
|
from autonomy.chain.base import registry_contracts
|
|
39
38
|
from autonomy.chain.config import CHAIN_PROFILES, ChainType
|
|
40
39
|
from autonomy.chain.metadata import IPFS_URI_PREFIX
|
|
41
40
|
from web3 import Web3
|
|
42
41
|
|
|
43
|
-
from operate.constants import
|
|
42
|
+
from operate.constants import (
|
|
43
|
+
AGENT_LOG_DIR,
|
|
44
|
+
AGENT_LOG_ENV_VAR,
|
|
45
|
+
AGENT_PERSISTENT_STORAGE_DIR,
|
|
46
|
+
AGENT_PERSISTENT_STORAGE_ENV_VAR,
|
|
47
|
+
IPFS_ADDRESS,
|
|
48
|
+
ZERO_ADDRESS,
|
|
49
|
+
)
|
|
44
50
|
from operate.data import DATA_DIR
|
|
45
51
|
from operate.data.contracts.mech_activity.contract import MechActivityContract
|
|
46
52
|
from operate.data.contracts.requester_activity_checker.contract import (
|
|
@@ -730,8 +736,8 @@ class ServiceManager:
|
|
|
730
736
|
|
|
731
737
|
# Set environment variables for the service
|
|
732
738
|
for dir_name, env_var_name in (
|
|
733
|
-
(
|
|
734
|
-
(
|
|
739
|
+
(AGENT_PERSISTENT_STORAGE_DIR, AGENT_PERSISTENT_STORAGE_ENV_VAR),
|
|
740
|
+
(AGENT_LOG_DIR, AGENT_LOG_ENV_VAR),
|
|
735
741
|
):
|
|
736
742
|
dir_path = service.path / dir_name
|
|
737
743
|
dir_path.mkdir(parents=True, exist_ok=True)
|
|
@@ -1318,18 +1324,17 @@ class ServiceManager:
|
|
|
1318
1324
|
service_config_id=service_config_id, chain=chain
|
|
1319
1325
|
)
|
|
1320
1326
|
self.logger.info("Swapping Safe owners")
|
|
1321
|
-
|
|
1322
|
-
|
|
1327
|
+
owner_crypto = self.keys_manager.get_crypto_instance(
|
|
1328
|
+
address=current_safe_owners[0]
|
|
1329
|
+
)
|
|
1330
|
+
sftxb.swap(
|
|
1331
|
+
service_id=chain_data.token,
|
|
1323
1332
|
multisig=chain_data.multisig, # TODO this can be read from the registry
|
|
1324
|
-
|
|
1325
|
-
self.keys_manager.get(
|
|
1326
|
-
key=current_safe_owners[0]
|
|
1327
|
-
).private_key # TODO allow multiple owners
|
|
1328
|
-
), # noqa: E800
|
|
1333
|
+
owner_cryptos=[owner_crypto], # TODO allow multiple owners
|
|
1329
1334
|
new_owner_address=(
|
|
1330
1335
|
safe if safe else wallet.crypto.address
|
|
1331
1336
|
), # TODO it should always be safe address
|
|
1332
|
-
)
|
|
1337
|
+
)
|
|
1333
1338
|
|
|
1334
1339
|
if withdrawal_address is not None:
|
|
1335
1340
|
ethereum_crypto = KeysManager().get_crypto_instance(
|
|
@@ -1457,26 +1462,18 @@ class ServiceManager:
|
|
|
1457
1462
|
if agent_is_service_safe_owner:
|
|
1458
1463
|
self.logger.info("(Agent) Enabling recovery module in service Safe.")
|
|
1459
1464
|
try:
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
)
|
|
1471
|
-
|
|
1472
|
-
module_address=recovery_module_address,
|
|
1473
|
-
safe_address=service_safe_address,
|
|
1474
|
-
)
|
|
1475
|
-
).settle()
|
|
1476
|
-
tmp_file.seek(0)
|
|
1477
|
-
tmp_file.write("\0" * len(private_key))
|
|
1478
|
-
tmp_file.flush()
|
|
1479
|
-
|
|
1465
|
+
crypto = self.keys_manager.get_crypto_instance(address=agent_address)
|
|
1466
|
+
EthSafeTxBuilder._new_tx( # pylint: disable=protected-access
|
|
1467
|
+
ledger_api=sftxb.ledger_api,
|
|
1468
|
+
crypto=crypto,
|
|
1469
|
+
chain_type=ChainType(chain),
|
|
1470
|
+
safe=service_safe_address,
|
|
1471
|
+
).add(
|
|
1472
|
+
sftxb.get_enable_module_data(
|
|
1473
|
+
module_address=recovery_module_address,
|
|
1474
|
+
safe_address=service_safe_address,
|
|
1475
|
+
)
|
|
1476
|
+
).settle()
|
|
1480
1477
|
self.logger.info(
|
|
1481
1478
|
"(Agent) Recovery module enabled successfully in service Safe."
|
|
1482
1479
|
)
|
|
@@ -2341,12 +2338,6 @@ class ServiceManager:
|
|
|
2341
2338
|
deployment.delete()
|
|
2342
2339
|
return deployment
|
|
2343
2340
|
|
|
2344
|
-
def log_directories(self) -> None:
|
|
2345
|
-
"""Log directories."""
|
|
2346
|
-
directories = [f" - {str(p)}" for p in self.path.iterdir() if p.is_dir()]
|
|
2347
|
-
directories_str = "\n".join(directories)
|
|
2348
|
-
self.logger.info(f"Directories in {self.path}\n: {directories_str}")
|
|
2349
|
-
|
|
2350
2341
|
def update(
|
|
2351
2342
|
self,
|
|
2352
2343
|
service_config_id: str,
|
operate/services/protocol.py
CHANGED
|
@@ -34,7 +34,6 @@ from typing import Optional, Union, cast
|
|
|
34
34
|
from aea.configurations.data_types import PackageType
|
|
35
35
|
from aea.crypto.base import Crypto, LedgerApi
|
|
36
36
|
from aea.helpers.base import IPFSHash, cd
|
|
37
|
-
from aea_ledger_ethereum.ethereum import EthereumCrypto
|
|
38
37
|
from autonomy.chain.base import registry_contracts
|
|
39
38
|
from autonomy.chain.config import ChainConfigs, ChainType, ContractConfigs
|
|
40
39
|
from autonomy.chain.constants import (
|
|
@@ -722,7 +721,11 @@ class _ChainUtil:
|
|
|
722
721
|
).get("owners", [])
|
|
723
722
|
|
|
724
723
|
def swap( # pylint: disable=too-many-arguments,too-many-locals
|
|
725
|
-
self,
|
|
724
|
+
self,
|
|
725
|
+
service_id: int,
|
|
726
|
+
multisig: str,
|
|
727
|
+
owner_cryptos: t.List[Crypto],
|
|
728
|
+
new_owner_address: str,
|
|
726
729
|
) -> None:
|
|
727
730
|
"""Swap safe owner."""
|
|
728
731
|
logging.info(f"Swapping safe for service {service_id} [{multisig}]...")
|
|
@@ -736,11 +739,6 @@ class _ChainUtil:
|
|
|
736
739
|
retries=ON_CHAIN_INTERACT_RETRIES,
|
|
737
740
|
sleep=ON_CHAIN_INTERACT_SLEEP,
|
|
738
741
|
)
|
|
739
|
-
with tempfile.TemporaryDirectory() as temp_dir:
|
|
740
|
-
key_file = Path(temp_dir, "key.txt")
|
|
741
|
-
key_file.write_text(owner_key, encoding="utf-8")
|
|
742
|
-
owner_crypto = EthereumCrypto(private_key_path=str(key_file))
|
|
743
|
-
owner_cryptos: t.List[EthereumCrypto] = [owner_crypto]
|
|
744
742
|
owners = [
|
|
745
743
|
manager.ledger_api.api.to_checksum_address(owner_crypto.address)
|
|
746
744
|
for owner_crypto in owner_cryptos
|
|
@@ -797,7 +795,7 @@ class _ChainUtil:
|
|
|
797
795
|
tx = registry_contracts.gnosis_safe.get_raw_safe_transaction(
|
|
798
796
|
ledger_api=manager.ledger_api,
|
|
799
797
|
contract_address=multisig,
|
|
800
|
-
sender_address=
|
|
798
|
+
sender_address=owner_cryptos[0].address,
|
|
801
799
|
owners=tuple(owners), # type: ignore
|
|
802
800
|
to_address=tx_params["to_address"],
|
|
803
801
|
value=tx_params["ether_value"],
|
|
@@ -806,7 +804,7 @@ class _ChainUtil:
|
|
|
806
804
|
signatures_by_owner=owner_to_signature,
|
|
807
805
|
operation=SafeOperation.DELEGATE_CALL.value,
|
|
808
806
|
)
|
|
809
|
-
stx =
|
|
807
|
+
stx = owner_cryptos[0].sign_transaction(tx)
|
|
810
808
|
tx_digest = manager.ledger_api.send_signed_transaction(stx)
|
|
811
809
|
receipt = manager.ledger_api.api.eth.wait_for_transaction_receipt(tx_digest)
|
|
812
810
|
if receipt["status"] != 1:
|
operate/services/service.py
CHANGED
|
@@ -63,7 +63,12 @@ from autonomy.deploy.generators.docker_compose.base import DockerComposeGenerato
|
|
|
63
63
|
from autonomy.deploy.generators.kubernetes.base import KubernetesGenerator
|
|
64
64
|
from docker import from_env
|
|
65
65
|
|
|
66
|
-
from operate.constants import
|
|
66
|
+
from operate.constants import (
|
|
67
|
+
AGENT_PERSISTENT_STORAGE_ENV_VAR,
|
|
68
|
+
CONFIG_JSON,
|
|
69
|
+
DEPLOYMENT_DIR,
|
|
70
|
+
DEPLOYMENT_JSON,
|
|
71
|
+
)
|
|
67
72
|
from operate.keys import KeysManager
|
|
68
73
|
from operate.operate_http.exceptions import NotAllowed
|
|
69
74
|
from operate.operate_types import (
|
|
@@ -98,75 +103,6 @@ SERVICE_CONFIG_PREFIX = "sc-"
|
|
|
98
103
|
NON_EXISTENT_MULTISIG = None
|
|
99
104
|
NON_EXISTENT_TOKEN = -1
|
|
100
105
|
|
|
101
|
-
DEFAULT_TRADER_ENV_VARS = {
|
|
102
|
-
"GNOSIS_LEDGER_RPC": {
|
|
103
|
-
"name": "Gnosis ledger RPC",
|
|
104
|
-
"description": "",
|
|
105
|
-
"value": "",
|
|
106
|
-
"provision_type": "computed",
|
|
107
|
-
},
|
|
108
|
-
"STAKING_CONTRACT_ADDRESS": {
|
|
109
|
-
"name": "Staking contract address",
|
|
110
|
-
"description": "",
|
|
111
|
-
"value": "",
|
|
112
|
-
"provision_type": "computed",
|
|
113
|
-
},
|
|
114
|
-
"MECH_MARKETPLACE_CONFIG": {
|
|
115
|
-
"name": "Mech marketplace configuration",
|
|
116
|
-
"description": "",
|
|
117
|
-
"value": "",
|
|
118
|
-
"provision_type": "computed",
|
|
119
|
-
},
|
|
120
|
-
"MECH_ACTIVITY_CHECKER_CONTRACT": {
|
|
121
|
-
"name": "Mech activity checker contract",
|
|
122
|
-
"description": "",
|
|
123
|
-
"value": "",
|
|
124
|
-
"provision_type": "computed",
|
|
125
|
-
},
|
|
126
|
-
"MECH_CONTRACT_ADDRESS": {
|
|
127
|
-
"name": "Mech contract address",
|
|
128
|
-
"description": "",
|
|
129
|
-
"value": "",
|
|
130
|
-
"provision_type": "computed",
|
|
131
|
-
},
|
|
132
|
-
"MECH_REQUEST_PRICE": {
|
|
133
|
-
"name": "Mech request price",
|
|
134
|
-
"description": "",
|
|
135
|
-
"value": "10000000000000000",
|
|
136
|
-
"provision_type": "computed",
|
|
137
|
-
},
|
|
138
|
-
"USE_MECH_MARKETPLACE": {
|
|
139
|
-
"name": "Use Mech marketplace",
|
|
140
|
-
"description": "",
|
|
141
|
-
"value": "False",
|
|
142
|
-
"provision_type": "computed",
|
|
143
|
-
},
|
|
144
|
-
"REQUESTER_STAKING_INSTANCE_ADDRESS": {
|
|
145
|
-
"name": "Requester staking instance address",
|
|
146
|
-
"description": "",
|
|
147
|
-
"value": "",
|
|
148
|
-
"provision_type": "computed",
|
|
149
|
-
},
|
|
150
|
-
"PRIORITY_MECH_ADDRESS": {
|
|
151
|
-
"name": "Priority Mech address",
|
|
152
|
-
"description": "",
|
|
153
|
-
"value": "",
|
|
154
|
-
"provision_type": "computed",
|
|
155
|
-
},
|
|
156
|
-
"TOOLS_ACCURACY_HASH": {
|
|
157
|
-
"name": "Tools accuracy hash",
|
|
158
|
-
"description": "",
|
|
159
|
-
"value": "QmWgsqncF22hPLNTyWtDzVoKPJ9gmgR1jcuLL5t31xyzzr",
|
|
160
|
-
"provision_type": "fixed",
|
|
161
|
-
},
|
|
162
|
-
"ACC_INFO_FIELDS_REQUESTS": {
|
|
163
|
-
"name": "Acc info fields requests",
|
|
164
|
-
"description": "",
|
|
165
|
-
"value": "nr_responses",
|
|
166
|
-
"provision_type": "fixed",
|
|
167
|
-
},
|
|
168
|
-
}
|
|
169
|
-
|
|
170
106
|
AGENT_TYPE_IDS = {"mech": 37, "optimus": 40, "modius": 40, "trader": 25}
|
|
171
107
|
|
|
172
108
|
|
|
@@ -996,6 +932,40 @@ class Service(LocalResource):
|
|
|
996
932
|
except Exception as e: # pylint: disable=broad-except
|
|
997
933
|
print(f"Exception deleting {healthcheck_json_path}: {e}")
|
|
998
934
|
|
|
935
|
+
def get_agent_performance(self) -> t.Dict:
|
|
936
|
+
"""Return the agent activity"""
|
|
937
|
+
|
|
938
|
+
# Default values
|
|
939
|
+
agent_performance: t.Dict[str, t.Any] = {
|
|
940
|
+
"timestamp": None,
|
|
941
|
+
"metrics": [],
|
|
942
|
+
"last_activity": None,
|
|
943
|
+
"last_chat_message": None,
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
agent_performance_json_path = (
|
|
947
|
+
Path(
|
|
948
|
+
self.env_variables.get(
|
|
949
|
+
AGENT_PERSISTENT_STORAGE_ENV_VAR, {"value": "."}
|
|
950
|
+
).get("value", ".")
|
|
951
|
+
)
|
|
952
|
+
/ "agent_performance.json"
|
|
953
|
+
)
|
|
954
|
+
|
|
955
|
+
if agent_performance_json_path.exists():
|
|
956
|
+
try:
|
|
957
|
+
with open(agent_performance_json_path, "r", encoding="utf-8") as f:
|
|
958
|
+
data = json.load(f)
|
|
959
|
+
if isinstance(data, dict):
|
|
960
|
+
agent_performance.update(data)
|
|
961
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
962
|
+
# Keep default values if file is invalid
|
|
963
|
+
print(
|
|
964
|
+
f"Error reading file 'agent_performance.json': {e}"
|
|
965
|
+
) # TODO Use logger
|
|
966
|
+
|
|
967
|
+
return dict(sorted(agent_performance.items()))
|
|
968
|
+
|
|
999
969
|
def update(
|
|
1000
970
|
self,
|
|
1001
971
|
service_template: ServiceTemplate,
|
operate/wallet/master.py
CHANGED
|
@@ -69,9 +69,12 @@ class MasterWallet(LocalResource):
|
|
|
69
69
|
"""Master wallet."""
|
|
70
70
|
|
|
71
71
|
path: Path
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
address: str
|
|
73
|
+
|
|
74
|
+
safes: t.Dict[Chain, str] = field(default_factory=dict)
|
|
75
|
+
safe_chains: t.List[Chain] = field(default_factory=list)
|
|
74
76
|
ledger_type: LedgerType
|
|
77
|
+
safe_nonce: t.Optional[int] = None
|
|
75
78
|
|
|
76
79
|
_key: str
|
|
77
80
|
_crypto: t.Optional[Crypto] = None
|
|
@@ -229,8 +232,8 @@ class EthereumMasterWallet(MasterWallet):
|
|
|
229
232
|
path: Path
|
|
230
233
|
address: str
|
|
231
234
|
|
|
232
|
-
safes: t.
|
|
233
|
-
safe_chains: t.List[Chain] = field(default_factory=list)
|
|
235
|
+
safes: t.Dict[Chain, str] = field(default_factory=dict)
|
|
236
|
+
safe_chains: t.List[Chain] = field(default_factory=list)
|
|
234
237
|
ledger_type: LedgerType = LedgerType.ETHEREUM
|
|
235
238
|
safe_nonce: t.Optional[int] = None # For cross-chain reusability
|
|
236
239
|
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# ------------------------------------------------------------------------------
|
|
3
|
+
#
|
|
4
|
+
# Copyright 2025 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 recovery manager"""
|
|
21
|
+
|
|
22
|
+
import shutil
|
|
23
|
+
import typing as t
|
|
24
|
+
import uuid
|
|
25
|
+
from logging import Logger
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
from operate.account.user import UserAccount
|
|
29
|
+
from operate.constants import USER_JSON, WALLETS_DIR
|
|
30
|
+
from operate.utils.gnosis import get_owners
|
|
31
|
+
from operate.wallet.master import MasterWalletManager
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
RECOVERY_BUNDLE_PREFIX = "eb-"
|
|
35
|
+
RECOVERY_NEW_OBJECTS_DIR = "tmp"
|
|
36
|
+
RECOVERY_OLD_OBJECTS_DIR = "old"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class WalletRecoveryError(Exception):
|
|
40
|
+
"""WalletRecoveryError"""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class WalletRecoveryManager:
|
|
44
|
+
"""WalletRecoveryManager"""
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
path: Path,
|
|
49
|
+
logger: Logger,
|
|
50
|
+
wallet_manager: MasterWalletManager,
|
|
51
|
+
) -> None:
|
|
52
|
+
"""Initialize master wallet manager."""
|
|
53
|
+
self.path = path
|
|
54
|
+
self.logger = logger
|
|
55
|
+
self.wallet_manager = wallet_manager
|
|
56
|
+
|
|
57
|
+
def initiate_recovery(self, new_password: str) -> t.Dict:
|
|
58
|
+
"""Recovery step 1"""
|
|
59
|
+
self.logger.info("[WALLET RECOVERY MANAGER] Recovery step 1 start")
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
_ = self.wallet_manager.password
|
|
63
|
+
except ValueError:
|
|
64
|
+
pass
|
|
65
|
+
else:
|
|
66
|
+
raise WalletRecoveryError(
|
|
67
|
+
"Wallet recovery cannot be executed while logged in."
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
if not new_password:
|
|
71
|
+
raise ValueError("'new_password' must be a non-empty string.")
|
|
72
|
+
|
|
73
|
+
bundle_id = f"{RECOVERY_BUNDLE_PREFIX}{str(uuid.uuid4())}"
|
|
74
|
+
new_root = self.path / bundle_id / RECOVERY_NEW_OBJECTS_DIR
|
|
75
|
+
new_root.mkdir(parents=True, exist_ok=False)
|
|
76
|
+
UserAccount.new(new_password, new_root / USER_JSON)
|
|
77
|
+
|
|
78
|
+
new_wallets_path = new_root / WALLETS_DIR
|
|
79
|
+
new_wallet_manager = MasterWalletManager(
|
|
80
|
+
path=new_wallets_path, logger=self.logger, password=new_password
|
|
81
|
+
)
|
|
82
|
+
new_wallet_manager.setup()
|
|
83
|
+
|
|
84
|
+
output = []
|
|
85
|
+
for wallet in self.wallet_manager:
|
|
86
|
+
ledger_type = wallet.ledger_type
|
|
87
|
+
new_wallet, new_mnemonic = new_wallet_manager.create(
|
|
88
|
+
ledger_type=ledger_type
|
|
89
|
+
)
|
|
90
|
+
self.logger.info(
|
|
91
|
+
f"[WALLET RECOVERY MANAGER] Created new wallet {ledger_type=} {new_wallet.address=}"
|
|
92
|
+
)
|
|
93
|
+
output.append(
|
|
94
|
+
{
|
|
95
|
+
"current_wallet": wallet.json,
|
|
96
|
+
"new_wallet": new_wallet.json,
|
|
97
|
+
"new_mnemonic": new_mnemonic,
|
|
98
|
+
}
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
self.logger.info("[WALLET RECOVERY MANAGER] Recovery step 1 finish")
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
"id": bundle_id,
|
|
105
|
+
"wallets": output,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
def complete_recovery( # pylint: disable=too-many-locals,too-many-statements
|
|
109
|
+
self, bundle_id: str, password: str, raise_if_inconsistent_owners: bool = True
|
|
110
|
+
) -> None:
|
|
111
|
+
"""Recovery step 2"""
|
|
112
|
+
self.logger.info("[WALLET RECOVERY MANAGER] Recovery step 2 start")
|
|
113
|
+
|
|
114
|
+
def _report_issue(msg: str) -> None:
|
|
115
|
+
self.logger.warning(f"[WALLET RECOVERY MANAGER] {msg}")
|
|
116
|
+
if raise_if_inconsistent_owners:
|
|
117
|
+
raise WalletRecoveryError(f"{msg}")
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
_ = self.wallet_manager.password
|
|
121
|
+
except ValueError:
|
|
122
|
+
pass
|
|
123
|
+
else:
|
|
124
|
+
raise WalletRecoveryError(
|
|
125
|
+
"Wallet recovery cannot be executed while logged in."
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
if not password:
|
|
129
|
+
raise ValueError("'password' must be a non-empty string.")
|
|
130
|
+
|
|
131
|
+
if not bundle_id:
|
|
132
|
+
raise ValueError("'bundle_id' must be a non-empty string.")
|
|
133
|
+
|
|
134
|
+
root = self.path.parent # .operate root
|
|
135
|
+
wallets_path = root / WALLETS_DIR
|
|
136
|
+
new_root = self.path / bundle_id / RECOVERY_NEW_OBJECTS_DIR
|
|
137
|
+
new_wallets_path = new_root / WALLETS_DIR
|
|
138
|
+
old_root = self.path / bundle_id / RECOVERY_OLD_OBJECTS_DIR
|
|
139
|
+
|
|
140
|
+
if not new_root.exists() or not new_root.is_dir():
|
|
141
|
+
raise KeyError(f"Recovery bundle {bundle_id} does not exist.")
|
|
142
|
+
|
|
143
|
+
if old_root.exists() and old_root.is_dir():
|
|
144
|
+
raise ValueError(f"Recovery bundle {bundle_id} has been executed already.")
|
|
145
|
+
|
|
146
|
+
new_user_account = UserAccount.load(new_root / USER_JSON)
|
|
147
|
+
if not new_user_account.is_valid(password=password):
|
|
148
|
+
raise ValueError("Password is not valid.")
|
|
149
|
+
|
|
150
|
+
new_wallet_manager = MasterWalletManager(
|
|
151
|
+
path=new_wallets_path, logger=self.logger, password=password
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
ledger_types = {item.ledger_type for item in self.wallet_manager}
|
|
155
|
+
new_ledger_types = {item.ledger_type for item in new_wallet_manager}
|
|
156
|
+
|
|
157
|
+
if ledger_types != new_ledger_types:
|
|
158
|
+
raise WalletRecoveryError(
|
|
159
|
+
f"Ledger type mismatch: {ledger_types=}, {new_ledger_types=}."
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
for wallet in self.wallet_manager:
|
|
163
|
+
new_wallet = next(
|
|
164
|
+
(w for w in new_wallet_manager if w.ledger_type == wallet.ledger_type)
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
all_backup_owners = set()
|
|
168
|
+
for chain, safe in wallet.safes.items():
|
|
169
|
+
ledger_api = wallet.ledger_api(chain=chain)
|
|
170
|
+
owners = get_owners(ledger_api=ledger_api, safe=safe)
|
|
171
|
+
if new_wallet.address not in owners:
|
|
172
|
+
raise WalletRecoveryError(
|
|
173
|
+
f"Incorrect owners. Wallet {new_wallet.address} is not an owner of Safe {safe} on {chain}."
|
|
174
|
+
)
|
|
175
|
+
if wallet.address in owners:
|
|
176
|
+
_report_issue(
|
|
177
|
+
f"Inconsistent owners. Current wallet {wallet.address} is still an owner of Safe {safe} on {chain}."
|
|
178
|
+
)
|
|
179
|
+
if len(owners) != 2:
|
|
180
|
+
_report_issue(
|
|
181
|
+
f"Inconsistent owners. Safe {safe} on {chain} has {len(owners)} != 2 owners."
|
|
182
|
+
)
|
|
183
|
+
all_backup_owners.update(set(owners) - {new_wallet.address})
|
|
184
|
+
|
|
185
|
+
if len(all_backup_owners) != 1:
|
|
186
|
+
_report_issue(
|
|
187
|
+
f"Inconsistent owners. Backup owners differ across Safes on chains {', '.join(chain.value for chain in wallet.safes.keys())}. "
|
|
188
|
+
f"Found backup owners: {', '.join(map(str, all_backup_owners))}."
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
new_wallet.safes = wallet.safes.copy()
|
|
192
|
+
new_wallet.safe_chains = wallet.safe_chains.copy()
|
|
193
|
+
new_wallet.safe_nonce = wallet.safe_nonce
|
|
194
|
+
new_wallet.store()
|
|
195
|
+
|
|
196
|
+
# Update configuration recovery
|
|
197
|
+
try:
|
|
198
|
+
old_root.mkdir(parents=True, exist_ok=False)
|
|
199
|
+
shutil.move(str(wallets_path), str(old_root))
|
|
200
|
+
for file in root.glob(f"{USER_JSON}*"):
|
|
201
|
+
shutil.move(str(file), str(old_root / file.name))
|
|
202
|
+
|
|
203
|
+
shutil.move(str(new_wallets_path), str(root))
|
|
204
|
+
for file in new_root.glob(f"{USER_JSON}*"):
|
|
205
|
+
shutil.move(str(file), str(root / file.name))
|
|
206
|
+
|
|
207
|
+
except Exception as e:
|
|
208
|
+
raise RuntimeError from e
|
|
209
|
+
|
|
210
|
+
self.logger.info("[WALLET RECOVERY MANAGER] Recovery step 2 finish")
|
{olas_operate_middleware-0.10.7.dist-info → olas_operate_middleware-0.10.8.dist-info}/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|