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.
@@ -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")