olas-operate-middleware 0.10.6__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.6.dist-info → olas_operate_middleware-0.10.8.dist-info}/METADATA +1 -1
- {olas_operate_middleware-0.10.6.dist-info → olas_operate_middleware-0.10.8.dist-info}/RECORD +24 -19
- operate/__init__.py +12 -0
- operate/cli.py +145 -23
- operate/constants.py +9 -0
- operate/data/contracts/recovery_module/__init__.py +20 -0
- operate/data/contracts/recovery_module/build/RecoveryModule.json +811 -0
- operate/data/contracts/recovery_module/contract.py +61 -0
- operate/data/contracts/recovery_module/contract.yaml +23 -0
- operate/keys.py +9 -3
- operate/ledger/__init__.py +38 -24
- operate/ledger/profiles.py +73 -59
- operate/migration.py +73 -14
- operate/operate_types.py +2 -0
- operate/quickstart/reset_password.py +3 -2
- operate/quickstart/run_service.py +7 -2
- operate/services/manage.py +189 -19
- operate/services/protocol.py +153 -76
- operate/services/service.py +40 -58
- operate/wallet/master.py +7 -4
- operate/wallet/wallet_recovery_manager.py +210 -0
- {olas_operate_middleware-0.10.6.dist-info → olas_operate_middleware-0.10.8.dist-info}/LICENSE +0 -0
- {olas_operate_middleware-0.10.6.dist-info → olas_operate_middleware-0.10.8.dist-info}/WHEEL +0 -0
- {olas_operate_middleware-0.10.6.dist-info → olas_operate_middleware-0.10.8.dist-info}/entry_points.txt +0 -0
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,63 +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
|
-
}
|
|
157
|
-
|
|
158
106
|
AGENT_TYPE_IDS = {"mech": 37, "optimus": 40, "modius": 40, "trader": 25}
|
|
159
107
|
|
|
160
108
|
|
|
@@ -984,6 +932,40 @@ class Service(LocalResource):
|
|
|
984
932
|
except Exception as e: # pylint: disable=broad-except
|
|
985
933
|
print(f"Exception deleting {healthcheck_json_path}: {e}")
|
|
986
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
|
+
|
|
987
969
|
def update(
|
|
988
970
|
self,
|
|
989
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.6.dist-info → olas_operate_middleware-0.10.8.dist-info}/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|