olas-operate-middleware 0.12.2__py3-none-any.whl → 0.13.1__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.
@@ -79,6 +79,7 @@ from operate.keys import KeysManager
79
79
  from operate.ledger import get_default_ledger_api, get_default_rpc
80
80
  from operate.operate_http.exceptions import NotAllowed
81
81
  from operate.operate_types import (
82
+ AgentRelease,
82
83
  Chain,
83
84
  ChainAmounts,
84
85
  ChainConfig,
@@ -107,7 +108,7 @@ from operate.utils.ssl import create_ssl_certificate
107
108
  SAFE_CONTRACT_ADDRESS = "safe_contract_address"
108
109
  ALL_PARTICIPANTS = "all_participants"
109
110
  CONSENSUS_THRESHOLD = "consensus_threshold"
110
- SERVICE_CONFIG_VERSION = 8
111
+ SERVICE_CONFIG_VERSION = 9
111
112
  SERVICE_CONFIG_PREFIX = "sc-"
112
113
 
113
114
  NON_EXISTENT_MULTISIG = None
@@ -328,6 +329,8 @@ class HostDeploymentGenerator(BaseDeploymentGenerator):
328
329
  use_acn: bool = False,
329
330
  ) -> "HostDeploymentGenerator":
330
331
  """Generate agent and tendermint configurations"""
332
+ self.build_dir.mkdir(exist_ok=True, parents=True)
333
+ (self.build_dir / "agent").mkdir(exist_ok=True, parents=True)
331
334
  agent = self.service_builder.generate_agent(agent_n=0)
332
335
  agent = {key: f"{value}" for key, value in agent.items()}
333
336
  (self.build_dir / "agent.json").write_text(
@@ -395,7 +398,7 @@ class Deployment(LocalResource):
395
398
  if source_path.exists():
396
399
  shutil.copy(source_path, destination_path)
397
400
 
398
- def _build_kubernetes(self, password: str, force: bool = True) -> None:
401
+ def _build_kubernetes(self, keys_manager: KeysManager, force: bool = True) -> None:
399
402
  """Build kubernetes deployment."""
400
403
  k8s_build = self.path / DEPLOYMENT_DIR / "abci_build_k8s"
401
404
  if k8s_build.exists() and force:
@@ -407,7 +410,7 @@ class Deployment(LocalResource):
407
410
  keys_file.write_text(
408
411
  json.dumps(
409
412
  [
410
- KeysManager().get(address).get_decrypted(password)
413
+ keys_manager.get_decrypted(address)
411
414
  for address in service.agent_addresses
412
415
  ],
413
416
  indent=4,
@@ -437,7 +440,7 @@ class Deployment(LocalResource):
437
440
 
438
441
  def _build_docker(
439
442
  self,
440
- password: str,
443
+ keys_manager: KeysManager,
441
444
  force: bool = True,
442
445
  chain: t.Optional[str] = None,
443
446
  ) -> None:
@@ -462,7 +465,7 @@ class Deployment(LocalResource):
462
465
  keys_file.write_text(
463
466
  json.dumps(
464
467
  [
465
- KeysManager().get(address).get_decrypted(password)
468
+ keys_manager.get_decrypted(address)
466
469
  for address in service.agent_addresses
467
470
  ],
468
471
  indent=4,
@@ -551,7 +554,13 @@ class Deployment(LocalResource):
551
554
  self.status = DeploymentStatus.BUILT
552
555
  self.store()
553
556
 
554
- def _build_host(self, force: bool = True, chain: t.Optional[str] = None) -> None:
557
+ def _build_host(
558
+ self,
559
+ keys_manager: KeysManager,
560
+ force: bool = True,
561
+ chain: t.Optional[str] = None,
562
+ with_tm: bool = True,
563
+ ) -> None:
555
564
  """Build host depployment."""
556
565
  build = self.path / DEPLOYMENT_DIR
557
566
  if build.exists() and not force:
@@ -585,10 +594,7 @@ class Deployment(LocalResource):
585
594
  keys_file = self.path / DEFAULT_KEYS_FILE
586
595
  keys_file.write_text(
587
596
  json.dumps(
588
- [
589
- KeysManager().get(address).json
590
- for address in service.agent_addresses
591
- ],
597
+ [keys_manager.get(address).json for address in service.agent_addresses],
592
598
  indent=4,
593
599
  ),
594
600
  encoding="utf-8",
@@ -608,15 +614,21 @@ class Deployment(LocalResource):
608
614
  consensus_threshold=None,
609
615
  )
610
616
 
611
- (
612
- HostDeploymentGenerator(
613
- service_builder=builder,
614
- build_dir=build.resolve(),
615
- use_tm_testnet_setup=True,
616
- )
617
- .generate_config_tendermint()
618
- .generate()
619
- .populate_private_keys()
617
+ deployement_generator = HostDeploymentGenerator(
618
+ service_builder=builder,
619
+ build_dir=build.resolve(),
620
+ use_tm_testnet_setup=True,
621
+ )
622
+ if with_tm:
623
+ deployement_generator.generate_config_tendermint()
624
+
625
+ deployement_generator.generate()
626
+ deployement_generator.populate_private_keys()
627
+
628
+ # Add keys
629
+ shutil.copy(
630
+ build / "ethereum_private_key.txt",
631
+ build / "agent" / "ethereum_private_key.txt",
620
632
  )
621
633
 
622
634
  except Exception as e:
@@ -629,7 +641,7 @@ class Deployment(LocalResource):
629
641
 
630
642
  def build(
631
643
  self,
632
- password: str,
644
+ keys_manager: KeysManager,
633
645
  use_docker: bool = False,
634
646
  use_kubernetes: bool = False,
635
647
  force: bool = True,
@@ -665,9 +677,9 @@ class Deployment(LocalResource):
665
677
  )
666
678
  service.consume_env_variables()
667
679
  if use_docker:
668
- self._build_docker(password=password, force=force, chain=chain)
680
+ self._build_docker(keys_manager=keys_manager, force=force, chain=chain)
669
681
  if use_kubernetes:
670
- self._build_kubernetes(password=password, force=force)
682
+ self._build_kubernetes(keys_manager=keys_manager, force=force)
671
683
  else:
672
684
  ssl_key_path, ssl_cert_path = create_ssl_certificate(
673
685
  ssl_dir=service.path / DEPLOYMENT_DIR / "ssl"
@@ -679,12 +691,23 @@ class Deployment(LocalResource):
679
691
  }
680
692
  )
681
693
  service.consume_env_variables()
682
- self._build_host(force=force, chain=chain)
694
+ is_aea = service.agent_release["is_aea"]
695
+ self._build_host(
696
+ keys_manager=keys_manager,
697
+ force=force,
698
+ chain=chain,
699
+ with_tm=is_aea,
700
+ )
683
701
 
684
702
  os.environ.clear()
685
703
  os.environ.update(original_env)
686
704
 
687
- def start(self, password: str, use_docker: bool = False) -> None:
705
+ def start(
706
+ self,
707
+ password: str,
708
+ use_docker: bool = False,
709
+ is_aea: bool = True,
710
+ ) -> None:
688
711
  """Start the service"""
689
712
  if self.status != DeploymentStatus.BUILT:
690
713
  raise NotAllowed(
@@ -697,13 +720,15 @@ class Deployment(LocalResource):
697
720
  try:
698
721
  if use_docker:
699
722
  run_deployment(
700
- build_dir=self.path / "deployment",
723
+ build_dir=self.path / DEPLOYMENT_DIR,
701
724
  detach=True,
702
725
  project_name=self.path.name,
703
726
  )
704
727
  else:
705
728
  run_host_deployment(
706
- build_dir=self.path / "deployment", password=password
729
+ build_dir=self.path / DEPLOYMENT_DIR,
730
+ password=password,
731
+ is_aea=is_aea,
707
732
  )
708
733
  except Exception:
709
734
  self.status = DeploymentStatus.BUILT
@@ -713,7 +738,12 @@ class Deployment(LocalResource):
713
738
  self.status = DeploymentStatus.DEPLOYED
714
739
  self.store()
715
740
 
716
- def stop(self, use_docker: bool = False, force: bool = False) -> None:
741
+ def stop(
742
+ self,
743
+ use_docker: bool = False,
744
+ force: bool = False,
745
+ is_aea: bool = True,
746
+ ) -> None:
717
747
  """Stop the deployment."""
718
748
  if self.status != DeploymentStatus.DEPLOYED and not force:
719
749
  return
@@ -723,11 +753,11 @@ class Deployment(LocalResource):
723
753
 
724
754
  if use_docker:
725
755
  stop_deployment(
726
- build_dir=self.path / "deployment",
756
+ build_dir=self.path / DEPLOYMENT_DIR,
727
757
  project_name=self.path.name,
728
758
  )
729
759
  else:
730
- stop_host_deployment(build_dir=self.path / "deployment")
760
+ stop_host_deployment(build_dir=self.path / DEPLOYMENT_DIR, is_aea=is_aea)
731
761
 
732
762
  self.status = DeploymentStatus.BUILT
733
763
  self.store()
@@ -744,21 +774,20 @@ class Deployment(LocalResource):
744
774
  class Service(LocalResource):
745
775
  """Service class."""
746
776
 
777
+ name: str
747
778
  version: int
748
779
  service_config_id: str
780
+ path: Path
781
+ package_path: Path
749
782
  hash: str
750
783
  hash_history: t.Dict[int, str]
784
+ agent_release: AgentRelease
751
785
  agent_addresses: t.List[str]
752
786
  home_chain: str
753
787
  chain_configs: ChainConfigs
754
788
  description: str
755
789
  env_variables: EnvVariables
756
790
 
757
- path: Path
758
- package_path: Path
759
-
760
- name: t.Optional[str] = None
761
-
762
791
  _helper: t.Optional[ServiceHelper] = None
763
792
  _deployment: t.Optional[Deployment] = None
764
793
 
@@ -887,6 +916,7 @@ class Service(LocalResource):
887
916
  path=package_absolute_path.parent,
888
917
  package_path=Path(package_absolute_path.name),
889
918
  env_variables=service_template["env_variables"],
919
+ agent_release=service_template["agent_release"],
890
920
  )
891
921
  service.store()
892
922
  return service
@@ -1047,6 +1077,8 @@ class Service(LocalResource):
1047
1077
  )
1048
1078
  self.package_path = Path(package_absolute_path.name)
1049
1079
 
1080
+ self.agent_release = service_template.get("agent_release", self.agent_release)
1081
+
1050
1082
  # env_variables
1051
1083
  if partial_update:
1052
1084
  for var, attrs in service_template.get("env_variables", {}).items():
operate/wallet/master.py CHANGED
@@ -901,14 +901,14 @@ class MasterWalletManager:
901
901
  return [wallet.json for wallet in self]
902
902
 
903
903
  @property
904
- def password(self) -> str:
904
+ def password(self) -> t.Optional[str]:
905
905
  """Password string."""
906
906
  if self._password is None:
907
907
  raise ValueError("Password not set.")
908
908
  return self._password
909
909
 
910
910
  @password.setter
911
- def password(self, value: str) -> None:
911
+ def password(self, value: t.Optional[str]) -> None:
912
912
  """Set password value."""
913
913
  self._password = value
914
914
 
@@ -22,17 +22,30 @@
22
22
  import shutil
23
23
  import typing as t
24
24
  import uuid
25
+ from dataclasses import dataclass, field
25
26
  from logging import Logger
26
27
  from pathlib import Path
27
28
 
28
29
  from operate.account.user import UserAccount
29
- from operate.constants import MSG_INVALID_PASSWORD, USER_JSON, WALLETS_DIR
30
- from operate.utils.gnosis import get_owners
30
+ from operate.constants import (
31
+ KEYS_DIR,
32
+ MSG_INVALID_PASSWORD,
33
+ USER_JSON,
34
+ WALLETS_DIR,
35
+ ZERO_ADDRESS,
36
+ )
37
+ from operate.keys import KeysManager
38
+ from operate.ledger import get_default_ledger_api
39
+ from operate.ledger.profiles import DEFAULT_RECOVERY_TOPUPS
40
+ from operate.operate_types import ChainAmounts
41
+ from operate.resource import LocalResource
42
+ from operate.services.manage import ServiceManager
43
+ from operate.utils.gnosis import get_asset_balance, get_owners
31
44
  from operate.wallet.master import MasterWalletManager
32
45
 
33
46
 
34
47
  RECOVERY_BUNDLE_PREFIX = "eb-"
35
- RECOVERY_NEW_OBJECTS_DIR = "tmp"
48
+ RECOVERY_NEW_OBJECTS_DIR = "new"
36
49
  RECOVERY_OLD_OBJECTS_DIR = "old"
37
50
 
38
51
 
@@ -40,6 +53,18 @@ class WalletRecoveryError(Exception):
40
53
  """WalletRecoveryError"""
41
54
 
42
55
 
56
+ @dataclass
57
+ class WalletRecoveryManagerData(LocalResource):
58
+ """BridgeManagerData"""
59
+
60
+ path: Path
61
+ version: int = 1
62
+ last_prepared_bundle_id: t.Optional[str] = None
63
+ new_agent_keys: t.Dict[str, t.Dict[str, str]] = field(default_factory=dict)
64
+
65
+ _file = "wallet_recovery.json"
66
+
67
+
43
68
  class WalletRecoveryManager:
44
69
  """WalletRecoveryManager"""
45
70
 
@@ -48,15 +73,28 @@ class WalletRecoveryManager:
48
73
  path: Path,
49
74
  logger: Logger,
50
75
  wallet_manager: MasterWalletManager,
76
+ service_manager: ServiceManager,
51
77
  ) -> None:
52
78
  """Initialize wallet recovery manager."""
53
79
  self.path = path
54
80
  self.logger = logger
55
81
  self.wallet_manager = wallet_manager
82
+ self.service_manager = service_manager
83
+
84
+ path.mkdir(parents=True, exist_ok=True)
85
+ file = path / WalletRecoveryManagerData._file
86
+ if not file.exists():
87
+ WalletRecoveryManagerData(path=path).store()
88
+
89
+ self.data: WalletRecoveryManagerData = t.cast(
90
+ WalletRecoveryManagerData, WalletRecoveryManagerData.load(path)
91
+ )
56
92
 
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")
93
+ def prepare_recovery( # pylint: disable=too-many-locals
94
+ self, new_password: str
95
+ ) -> t.Dict:
96
+ """Prepare recovery"""
97
+ self.logger.info("[WALLET RECOVERY MANAGER] Prepare recovery started.")
60
98
 
61
99
  try:
62
100
  _ = self.wallet_manager.password
@@ -70,6 +108,39 @@ class WalletRecoveryManager:
70
108
  if not new_password:
71
109
  raise ValueError("'new_password' must be a non-empty string.")
72
110
 
111
+ for wallet in self.wallet_manager:
112
+ for chain, safe in wallet.safes.items():
113
+ ledger_api = get_default_ledger_api(chain)
114
+ owners = get_owners(ledger_api=ledger_api, safe=safe)
115
+
116
+ if wallet.address not in owners:
117
+ self.logger.warning(
118
+ f"Wallet {wallet.address} is not an owner of Safe {safe} on {chain.value}. (Interrupted swapping of Safe owners?)"
119
+ )
120
+
121
+ backup_owners = set(owners) - {wallet.address}
122
+ if len(backup_owners) < 1:
123
+ raise WalletRecoveryError(
124
+ f"Safe {safe} on {chain.value} has less than 1 backup owner."
125
+ )
126
+
127
+ last_prepared_bundle_id = self.data.last_prepared_bundle_id
128
+ if last_prepared_bundle_id is not None:
129
+ (
130
+ _,
131
+ num_safes_with_new_wallet,
132
+ _,
133
+ _,
134
+ ) = self._get_swap_status(last_prepared_bundle_id)
135
+ if num_safes_with_new_wallet > 0:
136
+ self.logger.info(
137
+ f"[WALLET RECOVERY MANAGER] Uncompleted bundle {last_prepared_bundle_id} has Safes with new wallet."
138
+ )
139
+ return self._load_bundle(
140
+ bundle_id=last_prepared_bundle_id, new_password=new_password
141
+ )
142
+
143
+ # Create new recovery bundle
73
144
  bundle_id = f"{RECOVERY_BUNDLE_PREFIX}{str(uuid.uuid4())}"
74
145
  new_root = self.path / bundle_id / RECOVERY_NEW_OBJECTS_DIR
75
146
  new_root.mkdir(parents=True, exist_ok=False)
@@ -81,35 +152,198 @@ class WalletRecoveryManager:
81
152
  )
82
153
  new_wallet_manager.setup()
83
154
 
84
- output = []
85
155
  for wallet in self.wallet_manager:
86
156
  ledger_type = wallet.ledger_type
87
- new_wallet, new_mnemonic = new_wallet_manager.create(
88
- ledger_type=ledger_type
89
- )
157
+ new_wallet, _ = new_wallet_manager.create(ledger_type=ledger_type)
90
158
  self.logger.info(
91
159
  f"[WALLET RECOVERY MANAGER] Created new wallet {ledger_type=} {new_wallet.address=}"
92
160
  )
93
- output.append(
161
+
162
+ new_keys_manager = KeysManager(
163
+ path=new_root / KEYS_DIR, password=new_password, logger=self.logger
164
+ )
165
+
166
+ new_agent_keys = self.data.new_agent_keys
167
+ for service in self.service_manager.get_all_services()[0]:
168
+ service_config_id = service.service_config_id
169
+ new_agent_keys.setdefault(service_config_id, {})
170
+ for agent_address in service.agent_addresses:
171
+ new_agent_address = new_keys_manager.create()
172
+ new_agent_keys[service_config_id][agent_address] = new_agent_address
173
+
174
+ self.data.last_prepared_bundle_id = bundle_id
175
+ self.data.store()
176
+ self.logger.info(
177
+ "[WALLET RECOVERY MANAGER] Prepare recovery finished with new bundle."
178
+ )
179
+ return self._load_bundle(bundle_id=bundle_id, new_password=new_password)
180
+
181
+ def _get_swap_status(self, bundle_id: str) -> t.Tuple[int, int, int, int]:
182
+ new_root = self.path / bundle_id / RECOVERY_NEW_OBJECTS_DIR
183
+ new_wallets_path = new_root / WALLETS_DIR
184
+ new_wallet_manager = MasterWalletManager(path=new_wallets_path, password=None)
185
+
186
+ num_safes = 0
187
+ num_safes_with_new_wallet = 0
188
+ num_safes_with_old_wallet = 0
189
+ num_safes_with_both_wallets = 0
190
+
191
+ for wallet in self.wallet_manager:
192
+ new_wallet = next(
193
+ (w for w in new_wallet_manager if w.ledger_type == wallet.ledger_type)
194
+ )
195
+ for chain, safe in wallet.safes.items():
196
+ ledger_api = get_default_ledger_api(chain)
197
+ owners = get_owners(ledger_api=ledger_api, safe=safe)
198
+
199
+ num_safes += 1
200
+ if new_wallet.address in owners and wallet.address in owners:
201
+ num_safes_with_both_wallets += 1
202
+ if new_wallet.address in owners:
203
+ num_safes_with_new_wallet += 1
204
+ if wallet.address in owners:
205
+ num_safes_with_old_wallet += 1
206
+
207
+ return (
208
+ num_safes,
209
+ num_safes_with_new_wallet,
210
+ num_safes_with_old_wallet,
211
+ num_safes_with_both_wallets,
212
+ )
213
+
214
+ def _load_bundle(self, bundle_id: str, new_password: str) -> t.Dict:
215
+ new_root = self.path / bundle_id / RECOVERY_NEW_OBJECTS_DIR
216
+
217
+ new_user_account = UserAccount.load(new_root / USER_JSON)
218
+ if not new_user_account.is_valid(password=new_password):
219
+ raise ValueError(MSG_INVALID_PASSWORD)
220
+
221
+ new_wallets_path = new_root / WALLETS_DIR
222
+ new_wallet_manager = MasterWalletManager(
223
+ path=new_wallets_path, password=new_password
224
+ )
225
+
226
+ wallets = []
227
+ for wallet in self.wallet_manager:
228
+ ledger_type = wallet.ledger_type
229
+ new_wallet = new_wallet_manager.load(ledger_type=ledger_type)
230
+ new_mnemonic = None
231
+ if new_password:
232
+ new_mnemonic = new_wallet.decrypt_mnemonic(password=new_password)
233
+ wallets.append(
94
234
  {
95
235
  "current_wallet": wallet.json,
96
236
  "new_wallet": new_wallet.json,
97
237
  "new_mnemonic": new_mnemonic,
98
238
  }
99
239
  )
240
+ return {
241
+ "id": bundle_id,
242
+ "wallets": wallets,
243
+ }
100
244
 
101
- self.logger.info("[WALLET RECOVERY MANAGER] Recovery step 1 finish")
245
+ def recovery_requirements( # pylint: disable=too-many-locals
246
+ self,
247
+ ) -> t.Dict[str, t.Any]:
248
+ """Get recovery funding requirements for backup owners."""
249
+
250
+ bundle_id = self.data.last_prepared_bundle_id
251
+ if not bundle_id:
252
+ return {}
253
+
254
+ balances = ChainAmounts()
255
+ requirements = ChainAmounts()
256
+ pending_backup_owner_swaps: t.Dict = {}
257
+
258
+ new_root = self.path / bundle_id / RECOVERY_NEW_OBJECTS_DIR
259
+ new_wallets_path = new_root / WALLETS_DIR
260
+ new_wallet_manager = MasterWalletManager(path=new_wallets_path, password=None)
261
+
262
+ for wallet in self.wallet_manager:
263
+ new_wallet = next(
264
+ (w for w in new_wallet_manager if w.ledger_type == wallet.ledger_type)
265
+ )
266
+ for chain, safe in wallet.safes.items():
267
+ chain_str = chain.value
268
+ balances.setdefault(chain_str, {})
269
+ requirements.setdefault(chain_str, {})
270
+
271
+ ledger_api = get_default_ledger_api(chain)
272
+ owners = get_owners(ledger_api=ledger_api, safe=safe)
273
+ backup_owners = set(owners) - {wallet.address} - {new_wallet.address}
274
+
275
+ if len(backup_owners) != 1:
276
+ self.logger.warning(
277
+ f"[WALLET RECOVERY MANAGER] Safe {safe} on {chain.value} has unexpected number of backup owners: {len(backup_owners)}."
278
+ )
279
+
280
+ for backup_owner in backup_owners:
281
+ balances[chain_str].setdefault(backup_owner, {})
282
+ balances[chain_str][backup_owner][ZERO_ADDRESS] = get_asset_balance(
283
+ ledger_api=ledger_api,
284
+ asset_address=ZERO_ADDRESS,
285
+ address=backup_owner,
286
+ raise_on_invalid_address=False,
287
+ )
288
+ requirements[chain_str].setdefault(backup_owner, {}).setdefault(
289
+ ZERO_ADDRESS, 0
290
+ )
291
+ if new_wallet.address not in owners:
292
+ requirements[chain_str][backup_owner][
293
+ ZERO_ADDRESS
294
+ ] += DEFAULT_RECOVERY_TOPUPS[chain][ZERO_ADDRESS]
295
+ pending_backup_owner_swaps.setdefault(chain_str, [])
296
+ if safe not in pending_backup_owner_swaps[chain_str]:
297
+ pending_backup_owner_swaps[chain_str].append(safe)
298
+
299
+ refill_requirements = ChainAmounts.shortfalls(
300
+ requirements=requirements, balances=balances
301
+ )
302
+ is_refill_required = any(
303
+ amount > 0
304
+ for address in refill_requirements.values()
305
+ for assets in address.values()
306
+ for amount in assets.values()
307
+ )
102
308
 
103
309
  return {
104
- "id": bundle_id,
105
- "wallets": output,
310
+ "balances": balances,
311
+ "total_requirements": requirements,
312
+ "refill_requirements": refill_requirements,
313
+ "is_refill_required": is_refill_required,
314
+ "pending_backup_owner_swaps": pending_backup_owner_swaps,
315
+ }
316
+
317
+ def status(self) -> t.Dict[str, t.Any]:
318
+ """Get recovery status."""
319
+ bundle_id = self.data.last_prepared_bundle_id
320
+ if not bundle_id:
321
+ return {
322
+ "prepared": False,
323
+ "bundle_id": bundle_id,
324
+ "has_swaps": False,
325
+ "has_pending_swaps": False,
326
+ }
327
+
328
+ (
329
+ num_safes,
330
+ num_safes_with_new_wallet,
331
+ _,
332
+ _,
333
+ ) = self._get_swap_status(bundle_id)
334
+
335
+ return {
336
+ "prepared": bundle_id is not None,
337
+ "bundle_id": bundle_id,
338
+ "has_swaps": num_safes_with_new_wallet > 0,
339
+ "has_pending_swaps": num_safes_with_new_wallet < num_safes,
106
340
  }
107
341
 
108
342
  def complete_recovery( # pylint: disable=too-many-locals,too-many-statements
109
- self, bundle_id: str, password: str, raise_if_inconsistent_owners: bool = True
343
+ self, raise_if_inconsistent_owners: bool = True
110
344
  ) -> None:
111
- """Recovery step 2"""
112
- self.logger.info("[WALLET RECOVERY MANAGER] Recovery step 2 start")
345
+ """Complete recovery"""
346
+ self.logger.info("[WALLET RECOVERY MANAGER] Complete recovery started.")
113
347
 
114
348
  def _report_issue(msg: str) -> None:
115
349
  self.logger.warning(f"[WALLET RECOVERY MANAGER] {msg}")
@@ -125,31 +359,27 @@ class WalletRecoveryManager:
125
359
  "Wallet recovery cannot be executed while logged in."
126
360
  )
127
361
 
128
- if not password:
129
- raise ValueError("'password' must be a non-empty string.")
362
+ bundle_id = self.data.last_prepared_bundle_id
130
363
 
131
364
  if not bundle_id:
132
- raise ValueError("'bundle_id' must be a non-empty string.")
365
+ raise WalletRecoveryError("No prepared bundle found.")
133
366
 
134
367
  root = self.path.parent # .operate root
135
368
  wallets_path = root / WALLETS_DIR
136
369
  new_root = self.path / bundle_id / RECOVERY_NEW_OBJECTS_DIR
137
370
  new_wallets_path = new_root / WALLETS_DIR
371
+ new_keys_path = new_root / KEYS_DIR
138
372
  old_root = self.path / bundle_id / RECOVERY_OLD_OBJECTS_DIR
139
373
 
140
374
  if not new_root.exists() or not new_root.is_dir():
141
- raise KeyError(f"Recovery bundle {bundle_id} does not exist.")
375
+ raise RuntimeError(f"Recovery bundle {bundle_id} does not exist.")
142
376
 
143
377
  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(MSG_INVALID_PASSWORD)
378
+ raise RuntimeError(
379
+ f"Recovery bundle {bundle_id} has been executed already."
380
+ )
149
381
 
150
- new_wallet_manager = MasterWalletManager(
151
- path=new_wallets_path, password=password
152
- )
382
+ new_wallet_manager = MasterWalletManager(path=new_wallets_path, password=None)
153
383
 
154
384
  ledger_types = {item.ledger_type for item in self.wallet_manager}
155
385
  new_ledger_types = {item.ledger_type for item in new_wallet_manager}
@@ -166,7 +396,7 @@ class WalletRecoveryManager:
166
396
 
167
397
  all_backup_owners = set()
168
398
  for chain, safe in wallet.safes.items():
169
- ledger_api = wallet.ledger_api(chain=chain)
399
+ ledger_api = get_default_ledger_api(chain)
170
400
  owners = get_owners(ledger_api=ledger_api, safe=safe)
171
401
  if new_wallet.address not in owners:
172
402
  raise WalletRecoveryError(
@@ -196,15 +426,30 @@ class WalletRecoveryManager:
196
426
  # Update configuration recovery
197
427
  try:
198
428
  old_root.mkdir(parents=True, exist_ok=False)
199
- shutil.move(str(wallets_path), str(old_root))
429
+ shutil.move(wallets_path, old_root)
200
430
  for file in root.glob(f"{USER_JSON}*"):
201
- shutil.move(str(file), str(old_root / file.name))
431
+ shutil.move(file, old_root / file.name)
202
432
 
203
- shutil.move(str(new_wallets_path), str(root))
433
+ shutil.copytree(
434
+ new_wallets_path, root / new_wallets_path.name, dirs_exist_ok=True
435
+ )
204
436
  for file in new_root.glob(f"{USER_JSON}*"):
205
- shutil.move(str(file), str(root / file.name))
206
-
437
+ shutil.copy2(file, root / file.name)
438
+ for file in new_keys_path.iterdir():
439
+ shutil.copy2(file, root / KEYS_DIR / file.name)
440
+
441
+ new_agent_keys = self.data.new_agent_keys
442
+ for service in self.service_manager.get_all_services()[0]:
443
+ service_config_id = service.service_config_id
444
+ service.agent_addresses = [
445
+ new_agent_keys[service_config_id][addr]
446
+ for addr in service.agent_addresses
447
+ ]
448
+ service.store()
449
+
450
+ self.data.last_prepared_bundle_id = None
451
+ self.data.store()
207
452
  except Exception as e:
208
453
  raise RuntimeError from e
209
454
 
210
- self.logger.info("[WALLET RECOVERY MANAGER] Recovery step 2 finish")
455
+ self.logger.info("[WALLET RECOVERY MANAGER] Complete recovery finished.")