olas-operate-middleware 0.10.7__py3-none-any.whl → 0.10.9__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.9
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=REHgtFvaSs7jm7B0K1tPkEKco7Bmd06KQvYAQstFPyw,55783
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,20 +57,20 @@ 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
62
- operate/ledger/profiles.py,sha256=7aLLf0pJTIHerpXbwpjeKE9inuwiw0FG0GgTWU_0vGE,11977
63
- operate/migration.py,sha256=YIJ9P8CdW2IsMm4gW9GdAJ7EKMnUfUMUB5TItiQ0dH8,15323
60
+ operate/keys.py,sha256=mPZ2KZujPjHZ1RNVD2vkLzK_3q6J32o9q0WCB6Raxs4,5237
61
+ operate/ledger/__init__.py,sha256=s8GEcV-VtKdBQb9DqW7uf-tj_NpFNn13aNHCh0eqfaQ,3037
62
+ operate/ledger/profiles.py,sha256=LaYPyz-1Cy-KRRuXk60W0ZXQJ2cwoWmDxYrL_psIdKo,11993
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
67
67
  operate/pearl.py,sha256=yrTpSXLu_ML3qT-uNxq3kScOyo31JyxBujiSMfMUbcg,1690
68
68
  operate/quickstart/analyse_logs.py,sha256=K11AWWevkddUIUzTe75J3fYVS6aLfi6kT_dAI9OjrX8,4195
69
- operate/quickstart/claim_staking_rewards.py,sha256=AqfLMRef2YijQtWPaTuGwX2sOItNEkoyoi6Q9SICp_I,4026
69
+ operate/quickstart/claim_staking_rewards.py,sha256=K7X1Yq0mxe3qWmFLb1Xu9-Jghhml95lS_LpM_BXii0o,3533
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=cmZ2pb3P1pHZrOdLAYkM0Azxv81dZdNQwnanM5ag7Zo,118090
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.9.dist-info/LICENSE,sha256=mdBDB-mWKV5Cz4ejBzBiKqan6Z8zVLAh9xwM64O2FW4,11339
95
+ olas_operate_middleware-0.10.9.dist-info/METADATA,sha256=ljzrFEYIuZlVQMFuMZtspZeau_-1Yp0SvIeyPJiONP8,2035
96
+ olas_operate_middleware-0.10.9.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
97
+ olas_operate_middleware-0.10.9.dist-info/entry_points.txt,sha256=dM1g2I7ODApKQFcgl5J4NGA7pfBTo6qsUTXM-j2OLlw,44
98
+ olas_operate_middleware-0.10.9.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=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
@@ -85,6 +85,7 @@ class KeysManager(metaclass=SingletonMeta):
85
85
  )
86
86
  )
87
87
  )
88
+ private_key = key.private_key
88
89
  # Create temporary file with delete=False to handle it manually
89
90
  with tempfile.NamedTemporaryFile(
90
91
  dir=self.path,
@@ -92,16 +93,22 @@ class KeysManager(metaclass=SingletonMeta):
92
93
  suffix=".txt",
93
94
  delete=False, # Handle cleanup manually
94
95
  ) as temp_file:
95
- temp_file.write(key.private_key)
96
+ temp_file_name = temp_file.name
97
+ temp_file.write(private_key)
96
98
  temp_file.flush()
97
99
  temp_file.close() # Close the file before reading
98
100
 
99
101
  # Set proper file permissions (readable by owner only)
100
- os.chmod(temp_file.name, 0o600)
101
- crypto = EthereumCrypto(private_key_path=temp_file.name)
102
+ os.chmod(temp_file_name, 0o600)
103
+ crypto = EthereumCrypto(private_key_path=temp_file_name)
102
104
 
103
105
  try:
104
- os.unlink(temp_file.name) # Clean up the temporary file
106
+ with open(temp_file_name, "r+", encoding="utf-8") as f:
107
+ f.seek(0)
108
+ f.write("\0" * len(private_key))
109
+ f.flush()
110
+ f.close()
111
+ os.unlink(temp_file_name) # Clean up the temporary file
105
112
  except OSError as e:
106
113
  self.logger.error(f"Failed to delete temp file {temp_file.name}: {e}")
107
114
 
@@ -24,62 +24,65 @@ 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")
30
- BASE_PUBLIC_RPC = os.environ.get("BASE_RPC", "https://mainnet.base.org")
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")
33
- MODE_PUBLIC_RPC = os.environ.get("MODE_RPC", "https://mainnet.mode.network/")
34
-
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")
27
+ CHAINS = [
28
+ Chain.ARBITRUM_ONE,
29
+ Chain.BASE,
30
+ Chain.CELO,
31
+ Chain.ETHEREUM,
32
+ Chain.GNOSIS,
33
+ Chain.MODE,
34
+ Chain.OPTIMISM,
35
+ Chain.POLYGON,
36
+ Chain.SOLANA,
37
+ ]
38
+
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
- PUBLIC_RPCS = {
44
- Chain.ETHEREUM: ETHEREUM_PUBLIC_RPC,
45
- Chain.GNOSIS: GNOSIS_PUBLIC_RPC,
46
- Chain.SOLANA: SOLANA_PUBLIC_RPC,
47
- Chain.BASE: BASE_PUBLIC_RPC,
48
- Chain.CELO: CELO_PUBLIC_RPC,
49
- Chain.OPTIMISM: OPTIMISM_PUBLIC_RPC,
50
- Chain.MODE: MODE_PUBLIC_RPC,
51
- }
52
49
 
53
50
  DEFAULT_RPCS = {
54
- Chain.ETHEREUM: ETHEREUM_RPC,
55
- Chain.GNOSIS: GNOSIS_RPC,
56
- Chain.SOLANA: SOLANA_RPC,
51
+ Chain.ARBITRUM_ONE: ARBITRUM_ONE_RPC,
57
52
  Chain.BASE: BASE_RPC,
58
53
  Chain.CELO: CELO_RPC,
59
- Chain.OPTIMISM: OPTIMISM_RPC,
54
+ Chain.ETHEREUM: ETHEREUM_RPC,
55
+ Chain.GNOSIS: GNOSIS_RPC,
60
56
  Chain.MODE: MODE_RPC,
57
+ Chain.OPTIMISM: OPTIMISM_RPC,
58
+ Chain.POLYGON: POLYGON_RPC,
59
+ Chain.SOLANA: SOLANA_RPC,
61
60
  }
62
61
 
63
62
  # Base currency for each chain
64
63
  CURRENCY_DENOMS = {
65
- Chain.ETHEREUM: "ETH",
66
- Chain.GNOSIS: "xDAI",
67
- Chain.SOLANA: "SOL",
64
+ Chain.ARBITRUM_ONE: "ETH",
68
65
  Chain.BASE: "ETH",
69
66
  Chain.CELO: "CELO",
70
- Chain.OPTIMISM: "ETH",
67
+ Chain.ETHEREUM: "ETH",
68
+ Chain.GNOSIS: "xDAI",
71
69
  Chain.MODE: "ETH",
70
+ Chain.OPTIMISM: "ETH",
71
+ Chain.POLYGON: "POL",
72
+ Chain.SOLANA: "SOL",
72
73
  }
73
74
 
74
75
  # Smallest denomination for each chain
75
76
  CURRENCY_SMALLEST_UNITS = {
76
- Chain.ETHEREUM: "Wei",
77
- Chain.GNOSIS: "Wei",
78
- Chain.SOLANA: "Lamport",
77
+ Chain.ARBITRUM_ONE: "Wei",
79
78
  Chain.BASE: "Wei",
80
79
  Chain.CELO: "Wei",
81
- Chain.OPTIMISM: "Wei",
80
+ Chain.ETHEREUM: "Wei",
81
+ Chain.GNOSIS: "Wei",
82
82
  Chain.MODE: "Wei",
83
+ Chain.OPTIMISM: "Wei",
84
+ Chain.POLYGON: "Wei",
85
+ Chain.SOLANA: "Lamport",
83
86
  }
84
87
 
85
88
 
@@ -24,40 +24,33 @@ import typing as t
24
24
  from autonomy.chain.constants import CHAIN_PROFILES, DEFAULT_MULTISEND
25
25
 
26
26
  from operate.constants import NO_STAKING_PROGRAM_ID, ZERO_ADDRESS
27
+ from operate.ledger import CHAINS
27
28
  from operate.operate_types import Chain, ContractAddresses
28
29
 
29
30
 
30
31
  # TODO: Refactor, remove the usage of CONTRACTS and use CHAIN_PROFILES from Open Autonomy instead.
31
- CHAINS = [
32
- Chain.ARBITRUM_ONE,
33
- Chain.BASE,
34
- Chain.CELO,
35
- Chain.ETHEREUM,
36
- Chain.GNOSIS,
37
- Chain.MODE,
38
- Chain.OPTIMISM,
39
- Chain.POLYGON,
40
- ]
41
-
42
32
  CONTRACTS: t.Dict[Chain, ContractAddresses] = {}
43
33
  for _chain in CHAINS:
44
- profile = CHAIN_PROFILES[_chain.value]
45
- CONTRACTS[_chain] = ContractAddresses(
46
- {
47
- "service_registry": profile["service_registry"],
48
- "service_registry_token_utility": profile["service_registry_token_utility"],
49
- "service_manager": profile["service_manager_token"],
50
- "gnosis_safe_proxy_factory": profile["gnosis_safe_proxy_factory"],
51
- "gnosis_safe_same_address_multisig": profile[
52
- "gnosis_safe_same_address_multisig"
53
- ],
54
- "safe_multisig_with_recovery_module": profile[
55
- "safe_multisig_with_recovery_module"
56
- ],
57
- "recovery_module": profile["recovery_module"],
58
- "multisend": DEFAULT_MULTISEND,
59
- }
60
- )
34
+ if _chain.value in CHAIN_PROFILES:
35
+ profile = CHAIN_PROFILES[_chain.value]
36
+ CONTRACTS[_chain] = ContractAddresses(
37
+ {
38
+ "service_registry": profile["service_registry"],
39
+ "service_registry_token_utility": profile[
40
+ "service_registry_token_utility"
41
+ ],
42
+ "service_manager": profile["service_manager_token"],
43
+ "gnosis_safe_proxy_factory": profile["gnosis_safe_proxy_factory"],
44
+ "gnosis_safe_same_address_multisig": profile[
45
+ "gnosis_safe_same_address_multisig"
46
+ ],
47
+ "safe_multisig_with_recovery_module": profile[
48
+ "safe_multisig_with_recovery_module"
49
+ ],
50
+ "recovery_module": profile["recovery_module"],
51
+ "multisend": DEFAULT_MULTISEND,
52
+ }
53
+ )
61
54
 
62
55
  STAKING: t.Dict[Chain, t.Dict[str, str]] = {
63
56
  Chain.ARBITRUM_ONE: {},