olas-operate-middleware 0.9.0__py3-none-any.whl → 0.10.0__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.
operate/migration.py CHANGED
@@ -22,9 +22,25 @@
22
22
 
23
23
  import json
24
24
  import logging
25
+ import shutil
26
+ import traceback
25
27
  from pathlib import Path
28
+ from time import time
26
29
 
30
+ from aea_cli_ipfs.ipfs_utils import IPFSTool
31
+
32
+ from operate.constants import ZERO_ADDRESS
33
+ from operate.operate_types import Chain, LedgerType
34
+ from operate.services.manage import ServiceManager
35
+ from operate.services.service import (
36
+ DEFAULT_TRADER_ENV_VARS,
37
+ NON_EXISTENT_MULTISIG,
38
+ SERVICE_CONFIG_PREFIX,
39
+ SERVICE_CONFIG_VERSION,
40
+ Service,
41
+ )
27
42
  from operate.utils import create_backup
43
+ from operate.wallet.master import LEDGER_TYPE_TO_WALLET_CLASS, MasterWalletManager
28
44
 
29
45
 
30
46
  class MigrationManager:
@@ -42,6 +58,12 @@ class MigrationManager:
42
58
  self._path = home
43
59
  self.logger = logger
44
60
 
61
+ def log_directories(self, path: Path) -> None:
62
+ """Log directories present in `path`."""
63
+ directories = [f" - {str(p)}" for p in path.iterdir() if p.is_dir()]
64
+ directories_str = "\n".join(directories)
65
+ self.logger.info(f"Directories in {path}\n: {directories_str}")
66
+
45
67
  def migrate_user_account(self) -> None:
46
68
  """Migrates user.json"""
47
69
 
@@ -62,35 +84,280 @@ class MigrationManager:
62
84
 
63
85
  self.logger.info("[MIGRATION MANAGER] Migrated user.json.")
64
86
 
65
- def migrate_wallets(self) -> None:
66
- """Migrates wallets."""
87
+ def migrate_wallets(self, wallet_manager: MasterWalletManager) -> None:
88
+ """Migrate old wallet config formats to new ones, if applies."""
89
+ self.logger.info("Migrating wallet configs...")
67
90
 
68
- path = self._path / "wallets" / "ethereum.json"
69
- if not path.exists():
70
- return
91
+ for ledger_type in LedgerType:
92
+ if not wallet_manager.exists(ledger_type=ledger_type):
93
+ continue
71
94
 
72
- migrated = False
73
- with open(path, "r", encoding="utf-8") as f:
74
- data = json.load(f)
95
+ wallet_class = LEDGER_TYPE_TO_WALLET_CLASS.get(ledger_type)
96
+ if wallet_class is None:
97
+ continue
98
+
99
+ migrated = wallet_class.migrate_format(path=wallet_manager.path)
100
+ if migrated:
101
+ self.logger.info(f"Wallet {wallet_class} has been migrated.")
102
+
103
+ self.logger.info("Migrating wallet configs done.")
104
+
105
+ @staticmethod
106
+ def _migrate_service( # pylint: disable=too-many-statements,too-many-locals
107
+ path: Path,
108
+ ) -> bool:
109
+ """Migrate the JSON file format if needed."""
110
+
111
+ if not path.is_dir():
112
+ return False
113
+
114
+ if not path.name.startswith(SERVICE_CONFIG_PREFIX) and not path.name.startswith(
115
+ "bafybei"
116
+ ):
117
+ return False
118
+
119
+ if path.name.startswith("bafybei"):
120
+ backup_name = f"backup_{int(time())}_{path.name}"
121
+ backup_path = path.parent / backup_name
122
+ shutil.copytree(path, backup_path)
123
+ deployment_path = backup_path / "deployment"
124
+ if deployment_path.is_dir():
125
+ shutil.rmtree(deployment_path)
126
+
127
+ with open(
128
+ path / Service._file, # pylint: disable=protected-access
129
+ "r",
130
+ encoding="utf-8",
131
+ ) as file:
132
+ data = json.load(file)
133
+
134
+ version = data.get("version", 0)
135
+ if version > SERVICE_CONFIG_VERSION:
136
+ raise RuntimeError(
137
+ f"Service configuration in {path} has version {version}, which means it was created with a newer version of olas-operate-middleware. Only configuration versions <= {SERVICE_CONFIG_VERSION} are supported by this version of olas-operate-middleware."
138
+ )
139
+
140
+ # Complete missing env vars for trader
141
+ if "trader" in data["name"].lower():
142
+ data.setdefault("env_variables", {})
143
+
144
+ for key, value in DEFAULT_TRADER_ENV_VARS.items():
145
+ if key not in data["env_variables"]:
146
+ data["env_variables"][key] = value
147
+
148
+ with open(
149
+ path / Service._file, # pylint: disable=protected-access
150
+ "w",
151
+ encoding="utf-8",
152
+ ) as file:
153
+ json.dump(data, file, indent=2)
154
+
155
+ if version == SERVICE_CONFIG_VERSION:
156
+ return False
157
+
158
+ # Migration steps for older versions
159
+ if version == 0:
160
+ new_data = {
161
+ "version": 2,
162
+ "hash": data.get("hash"),
163
+ "keys": data.get("keys"),
164
+ "home_chain_id": "100", # This is the default value for version 2 - do not change, will be corrected below
165
+ "chain_configs": {
166
+ "100": { # This is the default value for version 2 - do not change, will be corrected below
167
+ "ledger_config": {
168
+ "rpc": data.get("ledger_config", {}).get("rpc"),
169
+ "type": data.get("ledger_config", {}).get("type"),
170
+ "chain": data.get("ledger_config", {}).get("chain"),
171
+ },
172
+ "chain_data": {
173
+ "instances": data.get("chain_data", {}).get(
174
+ "instances", []
175
+ ),
176
+ "token": data.get("chain_data", {}).get("token"),
177
+ "multisig": data.get("chain_data", {}).get("multisig"),
178
+ "staked": data.get("chain_data", {}).get("staked", False),
179
+ "on_chain_state": data.get("chain_data", {}).get(
180
+ "on_chain_state", 3
181
+ ),
182
+ "user_params": {
183
+ "staking_program_id": "pearl_alpha",
184
+ "nft": data.get("chain_data", {})
185
+ .get("user_params", {})
186
+ .get("nft"),
187
+ "threshold": data.get("chain_data", {})
188
+ .get("user_params", {})
189
+ .get("threshold"),
190
+ "use_staking": data.get("chain_data", {})
191
+ .get("user_params", {})
192
+ .get("use_staking"),
193
+ "cost_of_bond": data.get("chain_data", {})
194
+ .get("user_params", {})
195
+ .get("cost_of_bond"),
196
+ "fund_requirements": data.get("chain_data", {})
197
+ .get("user_params", {})
198
+ .get("fund_requirements", {}),
199
+ "agent_id": data.get("chain_data", {})
200
+ .get("user_params", {})
201
+ .get("agent_id", "14"),
202
+ },
203
+ },
204
+ }
205
+ },
206
+ "service_path": data.get("service_path", ""),
207
+ "name": data.get("name", ""),
208
+ }
209
+ data = new_data
210
+
211
+ if version < 4:
212
+ # Add missing fields introduced in later versions, if necessary.
213
+ for _, chain_data in data.get("chain_configs", {}).items():
214
+ chain_data.setdefault("chain_data", {}).setdefault(
215
+ "user_params", {}
216
+ ).setdefault("use_mech_marketplace", False)
217
+ service_name = data.get("name", "")
218
+ agent_id = Service.determine_agent_id(service_name)
219
+ chain_data.setdefault("chain_data", {}).setdefault("user_params", {})[
220
+ "agent_id"
221
+ ] = agent_id
75
222
 
76
- if "optimistic" in data.get("safes", {}):
77
- data["safes"]["optimism"] = data["safes"].pop("optimistic")
78
- migrated = True
223
+ data["description"] = data.setdefault("description", data.get("name"))
224
+ data["hash_history"] = data.setdefault(
225
+ "hash_history", {int(time()): data["hash"]}
226
+ )
227
+
228
+ if "service_config_id" not in data:
229
+ service_config_id = Service.get_new_service_config_id(path)
230
+ new_path = path.parent / service_config_id
231
+ data["service_config_id"] = service_config_id
232
+ path = path.rename(new_path)
233
+
234
+ old_to_new_ledgers = ["ethereum", "solana"]
235
+ for key_data in data["keys"]:
236
+ key_data["ledger"] = old_to_new_ledgers[key_data["ledger"]]
79
237
 
80
- if "optimistic" in data.get("safe_chains"):
81
- data["safe_chains"] = [
82
- "optimism" if chain == "optimistic" else chain
83
- for chain in data["safe_chains"]
238
+ old_to_new_chains = [
239
+ "ethereum",
240
+ "goerli",
241
+ "gnosis",
242
+ "solana",
243
+ "optimism",
244
+ "base",
245
+ "mode",
84
246
  ]
85
- migrated = True
247
+ new_chain_configs = {}
248
+ for chain_id, chain_data in data["chain_configs"].items():
249
+ chain_data["ledger_config"]["chain"] = old_to_new_chains[
250
+ chain_data["ledger_config"]["chain"]
251
+ ]
252
+ del chain_data["ledger_config"]["type"]
253
+ new_chain_configs[Chain.from_id(int(chain_id)).value] = chain_data # type: ignore
86
254
 
87
- if not migrated:
88
- return
255
+ data["chain_configs"] = new_chain_configs
256
+ data["home_chain"] = data.setdefault("home_chain", Chain.from_id(int(data.get("home_chain_id", "100"))).value) # type: ignore
257
+ del data["home_chain_id"]
89
258
 
90
- with open(path, "w", encoding="utf-8") as f:
91
- json.dump(data, f, indent=4)
259
+ if "env_variables" not in data:
260
+ if data["name"] == "valory/trader_pearl":
261
+ data["env_variables"] = DEFAULT_TRADER_ENV_VARS
262
+ else:
263
+ data["env_variables"] = {}
264
+
265
+ if version < 5:
266
+ new_chain_configs = {}
267
+ for chain, chain_data in data["chain_configs"].items():
268
+ fund_requirements = chain_data["chain_data"]["user_params"][
269
+ "fund_requirements"
270
+ ]
271
+ if ZERO_ADDRESS not in fund_requirements:
272
+ chain_data["chain_data"]["user_params"]["fund_requirements"] = {
273
+ ZERO_ADDRESS: fund_requirements
274
+ }
275
+
276
+ new_chain_configs[chain] = chain_data # type: ignore
277
+ data["chain_configs"] = new_chain_configs
278
+
279
+ if version < 7:
280
+ for _, chain_data in data.get("chain_configs", {}).items():
281
+ if chain_data["chain_data"]["multisig"] == "0xm":
282
+ chain_data["chain_data"]["multisig"] = NON_EXISTENT_MULTISIG
283
+
284
+ data["agent_addresses"] = [key["address"] for key in data["keys"]]
285
+ del data["keys"]
286
+
287
+ if version < 8:
288
+ if data["home_chain"] == "optimistic":
289
+ data["home_chain"] = Chain.OPTIMISM.value
290
+
291
+ if "optimistic" in data["chain_configs"]:
292
+ data["chain_configs"]["optimism"] = data["chain_configs"].pop(
293
+ "optimistic"
294
+ )
295
+
296
+ for _, chain_config in data["chain_configs"].items():
297
+ if chain_config["ledger_config"]["chain"] == "optimistic":
298
+ chain_config["ledger_config"]["chain"] = Chain.OPTIMISM.value
299
+
300
+ data["version"] = SERVICE_CONFIG_VERSION
301
+
302
+ # Redownload service path
303
+ if "service_path" in data:
304
+ package_absolute_path = path / Path(data["service_path"]).name
305
+ data.pop("service_path")
306
+ else:
307
+ package_absolute_path = path / data["package_path"]
308
+
309
+ if package_absolute_path.exists() and package_absolute_path.is_dir():
310
+ shutil.rmtree(package_absolute_path)
311
+
312
+ package_absolute_path = Path(
313
+ IPFSTool().download(
314
+ hash_id=data["hash"],
315
+ target_dir=path,
316
+ )
317
+ )
318
+ data["package_path"] = str(package_absolute_path.name)
319
+
320
+ with open(
321
+ path / Service._file, # pylint: disable=protected-access
322
+ "w",
323
+ encoding="utf-8",
324
+ ) as file:
325
+ json.dump(data, file, indent=2)
326
+
327
+ return True
328
+
329
+ def migrate_services(self, service_manager: ServiceManager) -> None:
330
+ """Migrate old service config formats to new ones, if applies."""
331
+ self.log_directories(service_manager.path)
332
+ self.logger.info("Migrating service configs...")
333
+
334
+ bafybei_count = sum(
335
+ 1
336
+ for path in service_manager.path.iterdir()
337
+ if path.name.startswith("bafybei")
338
+ )
339
+ if bafybei_count > 1:
340
+ raise RuntimeError(
341
+ f"Your services folder contains {bafybei_count} folders starting with 'bafybei'. This is an unintended situation. Please contact support."
342
+ )
343
+
344
+ paths = list(service_manager.path.iterdir())
345
+ for path in paths:
346
+ 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.")
354
+ except Exception as e: # pylint: disable=broad-except
355
+ self.logger.error(
356
+ f"Failed to migrate service: {path.name}. Exception {e}: {traceback.format_exc()}"
357
+ )
92
358
 
93
- self.logger.info("[MIGRATION MANAGER] Migrated wallets.")
359
+ self.logger.info("Migrating service configs done.")
360
+ self.log_directories(service_manager.path)
94
361
 
95
362
  def migrate_qs_configs(self) -> None:
96
363
  """Migrates quickstart configs."""
operate/operate_types.py CHANGED
@@ -28,6 +28,7 @@ from autonomy.chain.config import ChainType
28
28
  from autonomy.chain.constants import CHAIN_NAME_TO_CHAIN_ID
29
29
  from typing_extensions import TypedDict
30
30
 
31
+ from operate.constants import NO_STAKING_PROGRAM_ID
31
32
  from operate.resource import LocalResource
32
33
 
33
34
 
@@ -187,9 +188,6 @@ class ConfigurationTemplate(TypedDict):
187
188
  nft: str
188
189
  rpc: str
189
190
  agent_id: int
190
- threshold: int
191
- use_staking: bool
192
- use_mech_marketplace: bool
193
191
  cost_of_bond: int
194
192
  fund_requirements: t.Dict[str, FundRequirementsTemplate]
195
193
  fallback_chain_params: t.Optional[t.Dict]
@@ -254,13 +252,18 @@ class OnChainUserParams(LocalResource):
254
252
 
255
253
  staking_program_id: str
256
254
  nft: str
257
- threshold: int
258
255
  agent_id: int
259
- use_staking: bool
260
- use_mech_marketplace: bool
261
256
  cost_of_bond: int
262
257
  fund_requirements: OnChainTokenRequirements
263
258
 
259
+ @property
260
+ def use_staking(self) -> bool:
261
+ """Check if staking is used."""
262
+ return (
263
+ self.staking_program_id is not None
264
+ and self.staking_program_id != NO_STAKING_PROGRAM_ID
265
+ )
266
+
264
267
  @classmethod
265
268
  def from_json(cls, obj: t.Dict) -> "OnChainUserParams":
266
269
  """Load a service"""
@@ -38,7 +38,7 @@ def find_build_directory(config_file: Path, operate: "OperateApp") -> Path:
38
38
  config = json.load(f)
39
39
  config_service_hash = config.get("hash")
40
40
 
41
- services = operate.service_manager()._get_all_services()
41
+ services = operate.service_manager().get_all_services()
42
42
  for service in services:
43
43
  if service.hash == config_service_hash:
44
44
  build_dir = service.path / DEPLOYMENT_DIR
@@ -107,9 +107,6 @@ def analyse_logs(
107
107
  print(f"Config file '{config_file}' not found.")
108
108
  sys.exit(1)
109
109
 
110
- operate.service_manager().migrate_service_configs()
111
- operate.wallet_manager.migrate_wallet_configs()
112
-
113
110
  # Auto-detect the logs directory
114
111
  build_dir = find_build_directory(config_file, operate)
115
112
  logs_dir = build_dir / "persistent_data" / "logs"
@@ -48,9 +48,6 @@ def claim_staking_rewards(operate: "OperateApp", config_path: str) -> None:
48
48
 
49
49
  print_section(f"Claim staking rewards for {template['name']}")
50
50
 
51
- operate.service_manager().migrate_service_configs()
52
- operate.wallet_manager.migrate_wallet_configs()
53
-
54
51
  # check if agent was started before
55
52
  config = load_local_config(
56
53
  operate=operate, service_name=cast(str, template["name"])
@@ -70,9 +70,6 @@ def reset_configs(operate: "OperateApp", config_path: str) -> None:
70
70
  with open(config_path, "r") as config_file:
71
71
  template = json.load(config_file)
72
72
 
73
- operate.service_manager().migrate_service_configs()
74
- operate.wallet_manager.migrate_wallet_configs()
75
-
76
73
  print_title(f"Reset your {template['name']} configurations")
77
74
 
78
75
  # check if agent was started before
@@ -35,9 +35,6 @@ def reset_password(operate: "OperateApp") -> None:
35
35
  """Reset password."""
36
36
  print_title("Reset your password")
37
37
 
38
- operate.service_manager().migrate_service_configs()
39
- operate.wallet_manager.migrate_wallet_configs()
40
-
41
38
  # check if agent was started before
42
39
  if not (operate._path / "user.json").exists():
43
40
  print("No previous agent setup found. Exiting.")
@@ -22,7 +22,8 @@ import json
22
22
  import os
23
23
  from typing import TYPE_CHECKING, cast
24
24
 
25
- from operate.ledger.profiles import NO_STAKING_PROGRAM_ID, get_staking_contract
25
+ from operate.constants import NO_STAKING_PROGRAM_ID
26
+ from operate.ledger.profiles import get_staking_contract
26
27
  from operate.quickstart.run_service import (
27
28
  CUSTOM_PROGRAM_ID,
28
29
  ask_password_if_needed,
@@ -44,9 +45,6 @@ def reset_staking(operate: "OperateApp", config_path: str) -> None:
44
45
  with open(config_path, "r") as config_file:
45
46
  template = json.load(config_file)
46
47
 
47
- operate.service_manager().migrate_service_configs()
48
- operate.wallet_manager.migrate_wallet_configs()
49
-
50
48
  print_title("Reset your staking program preference")
51
49
 
52
50
  # check if agent was started before
@@ -34,10 +34,10 @@ 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, OPERATE_HOME
37
+ from operate.constants import IPFS_ADDRESS, NO_STAKING_PROGRAM_ID, OPERATE_HOME
38
38
  from operate.data import DATA_DIR
39
39
  from operate.data.contracts.staking_token.contract import StakingTokenContract
40
- from operate.ledger.profiles import NO_STAKING_PROGRAM_ID, STAKING, get_staking_contract
40
+ from operate.ledger.profiles import STAKING, get_staking_contract
41
41
  from operate.operate_types import (
42
42
  Chain,
43
43
  LedgerType,
@@ -363,14 +363,12 @@ def configure_local_config(
363
363
  template["configurations"][chain] |= {
364
364
  "staking_program_id": config.staking_program_id,
365
365
  "rpc": config.rpc[chain],
366
- "use_staking": config.staking_program_id != NO_STAKING_PROGRAM_ID,
367
366
  "cost_of_bond": min_staking_deposit,
368
367
  }
369
368
  else:
370
369
  template["configurations"][chain] |= {
371
370
  "staking_program_id": NO_STAKING_PROGRAM_ID,
372
371
  "rpc": config.rpc[chain],
373
- "use_staking": False,
374
372
  "cost_of_bond": 1,
375
373
  }
376
374
 
@@ -569,7 +567,7 @@ def _ask_funds_from_requirements(
569
567
  chain_config.chain_data.multisig: "Service Safe"
570
568
  for chain_config in service.chain_configs.values()
571
569
  }
572
- | {key.address: "Agent EOA" for key in service.keys}
570
+ | {address: "Agent EOA" for address in service.agent_addresses}
573
571
  )
574
572
 
575
573
  if not requirements["is_refill_required"] and requirements["allow_start_agent"]:
@@ -45,9 +45,6 @@ def stop_service(operate: "OperateApp", config_path: str) -> None:
45
45
 
46
46
  print_title(f"Stop {template['name']} Quickstart")
47
47
 
48
- operate.service_manager().migrate_service_configs()
49
- operate.wallet_manager.migrate_wallet_configs()
50
-
51
48
  # check if agent was started before
52
49
  config = load_local_config(
53
50
  operate=operate, service_name=cast(str, template["name"])
@@ -43,9 +43,6 @@ def terminate_service(operate: "OperateApp", config_path: str) -> None:
43
43
 
44
44
  print_title(f"Terminate {template['name']} on-chain service")
45
45
 
46
- operate.service_manager().migrate_service_configs()
47
- operate.wallet_manager.migrate_wallet_configs()
48
-
49
46
  # check if agent was started before
50
47
  config = load_local_config(
51
48
  operate=operate, service_name=cast(str, template["name"])