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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: olas-operate-middleware
3
- Version: 0.10.7
3
+ Version: 0.10.8
4
4
  Summary:
5
5
  Author: David Vilela
6
6
  Author-email: dvilelaf@gmail.com
@@ -1,4 +1,4 @@
1
- operate/__init__.py,sha256=ZQhHXOo_1L9Q5Ub_2FrZ-vu3BvORBrKgK8QcJyAsYa4,870
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=EalpBA15TXX5i3tD-2KIQOxkFwEOqOEiGMbjh36cOHw,51168
10
- operate/constants.py,sha256=syGWEVyw07zK50gPS4OSSwyAbt9R6P8IttDimDt9vCY,2746
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=JdnFFbzBOe9t4eS7i72EShOe25OkQdzRm_asCQHmmH0,4944
61
- operate/ledger/__init__.py,sha256=ksyctDd5PU_SToN9e-_N9fAap9ZNCHw48j5hHep-erA,3353
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=YIJ9P8CdW2IsMm4gW9GdAJ7EKMnUfUMUB5TItiQ0dH8,15323
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=p_gNmhWD4hb-QXUAiQRalPtVTVyB5TEPhs7GnScYzqs,2535
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=6opWEeedraCsPb6jEd44oV34jVQZJSNH8WOr-Kmo2LM,27116
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=CN_NFvXHbzoyGGyLRxmLVs_ghfxiXW1JlErUo1XRyeg,116402
83
- operate/services/protocol.py,sha256=FcHXPgeyOul3_3V5yF44fkxe79Ngpb7IaQh09dBOe0c,63206
84
- operate/services/service.py,sha256=ztZQToB2tfCUOUVSU2WJejIw3mSP6ZOUBmyB3ZmcLkI,40053
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=MGXynIV0LQU3n28AceK7RlaxAtfsVowsXzisesG4iJM,30760
93
- olas_operate_middleware-0.10.7.dist-info/LICENSE,sha256=mdBDB-mWKV5Cz4ejBzBiKqan6Z8zVLAh9xwM64O2FW4,11339
94
- olas_operate_middleware-0.10.7.dist-info/METADATA,sha256=W-w08wQZ3IFB9xxQrH0dO671Gu2SDNWmmgW1HEWASiQ,2035
95
- olas_operate_middleware-0.10.7.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
96
- olas_operate_middleware-0.10.7.dist-info/entry_points.txt,sha256=dM1g2I7ODApKQFcgl5J4NGA7pfBTo6qsUTXM-j2OLlw,44
97
- olas_operate_middleware-0.10.7.dist-info/RECORD,,
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=self.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, self.logger)
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 / "user.json",
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("'password' is required.")
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("'password' is required.")
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=self.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
- return (
182
- UserAccount.load(self._path / "user.json")
183
- if (self._path / "user.json").exists()
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 master wallet."""
197
+ """Load wallet manager."""
190
198
  manager = MasterWalletManager(
191
- path=self._path / "wallets",
199
+ path=self._path / WALLETS_DIR,
192
200
  password=self.password,
193
- logger=self.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 master wallet."""
218
+ """Load bridge manager."""
201
219
  manager = BridgeManager(
202
220
  path=self._path / "bridge",
203
221
  wallet_manager=self.wallet_manager,
204
- logger=self.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, logger=logger)
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(temp_file.name, 0o600)
101
- crypto = EthereumCrypto(private_key_path=temp_file.name)
101
+ os.chmod(temp_file_name, 0o600)
102
+ crypto = EthereumCrypto(private_key_path=temp_file_name)
102
103
 
103
104
  try:
104
- os.unlink(temp_file.name) # Clean up the temporary file
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
 
@@ -24,62 +24,76 @@ import os
24
24
  from operate.operate_types import Chain
25
25
 
26
26
 
27
- ETHEREUM_PUBLIC_RPC = os.environ.get("ETHEREUM_RPC", "https://ethereum.publicnode.com")
28
- GNOSIS_PUBLIC_RPC = os.environ.get("GNOSIS_RPC", "https://gnosis-rpc.publicnode.com")
29
- SOLANA_PUBLIC_RPC = os.environ.get("SOLANA_RPC", "https://api.mainnet-beta.solana.com")
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
- OPTIMISM_PUBLIC_RPC = os.environ.get("OPTIMISM_RPC", "https://mainnet.optimism.io")
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
- ETHEREUM_RPC = os.environ.get("ETHEREUM_RPC", "https://ethereum.publicnode.com")
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
- OPTIMISM_RPC = os.environ.get("OPTIMISM_RPC", "https://mainnet.optimism.io")
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.ETHEREUM: ETHEREUM_PUBLIC_RPC,
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.OPTIMISM: OPTIMISM_PUBLIC_RPC,
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.ETHEREUM: ETHEREUM_RPC,
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.OPTIMISM: OPTIMISM_RPC,
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.ETHEREUM: "ETH",
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.OPTIMISM: "ETH",
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.ETHEREUM: "Wei",
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.OPTIMISM: "Wei",
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}\n: {directories_str}")
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 / "user.json"
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
- if path.name.startswith(SERVICE_CONFIG_PREFIX) or path.name.startswith(
348
- "bafybei"
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 / "user.json").exists():
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 / "user.json",
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 IPFS_ADDRESS, NO_STAKING_PROGRAM_ID, OPERATE_HOME
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 / "user.json",
461
+ path=operate._path / USER_JSON,
457
462
  )
458
463
  else:
459
464
  _password = None
@@ -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 EthereumCrypto, LedgerApi
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 IPFS_ADDRESS, ZERO_ADDRESS
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
- ("persistent_data", "STORE_PATH"),
734
- ("benchmarks", "LOG_DIR"),
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
- sftxb.swap( # noqa: E800
1322
- service_id=chain_data.token, # noqa: E800
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
- owner_key=str(
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
- ) # noqa: E800
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
- with tempfile.NamedTemporaryFile(mode="w+", delete=True) as tmp_file:
1461
- private_key = self.keys_manager.get(key=agent_address).private_key
1462
- tmp_file.write(private_key)
1463
- tmp_file.flush()
1464
- crypto = EthereumCrypto(private_key_path=tmp_file.name)
1465
- EthSafeTxBuilder._new_tx( # pylint: disable=protected-access
1466
- ledger_api=sftxb.ledger_api,
1467
- crypto=crypto,
1468
- chain_type=ChainType(chain),
1469
- safe=service_safe_address,
1470
- ).add(
1471
- sftxb.get_enable_module_data(
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,
@@ -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, service_id: int, multisig: str, owner_key: str, new_owner_address: str
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=owner_crypto.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 = owner_crypto.sign_transaction(tx)
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:
@@ -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 CONFIG_JSON, DEPLOYMENT_DIR, DEPLOYMENT_JSON
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
- safes: t.Optional[t.Dict[Chain, str]] = {}
73
- safe_chains: t.List[Chain] = []
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.Optional[t.Dict[Chain, str]] = field(default_factory=dict) # type: ignore
233
- safe_chains: t.List[Chain] = field(default_factory=list) # type: ignore
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")