olas-operate-middleware 0.12.1__py3-none-any.whl → 0.13.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.
- {olas_operate_middleware-0.12.1.dist-info → olas_operate_middleware-0.13.0.dist-info}/METADATA +1 -1
- {olas_operate_middleware-0.12.1.dist-info → olas_operate_middleware-0.13.0.dist-info}/RECORD +20 -20
- operate/cli.py +73 -18
- operate/keys.py +9 -3
- operate/ledger/profiles.py +11 -0
- operate/migration.py +46 -12
- operate/operate_types.py +16 -0
- operate/quickstart/reset_password.py +1 -2
- operate/quickstart/run_service.py +17 -3
- operate/quickstart/stop_service.py +9 -2
- operate/services/agent_runner.py +19 -29
- operate/services/deployment_runner.py +83 -49
- operate/services/funding_manager.py +5 -3
- operate/services/manage.py +22 -7
- operate/services/service.py +66 -34
- operate/wallet/master.py +2 -2
- operate/wallet/wallet_recovery_manager.py +281 -36
- {olas_operate_middleware-0.12.1.dist-info → olas_operate_middleware-0.13.0.dist-info}/WHEEL +0 -0
- {olas_operate_middleware-0.12.1.dist-info → olas_operate_middleware-0.13.0.dist-info}/entry_points.txt +0 -0
- {olas_operate_middleware-0.12.1.dist-info → olas_operate_middleware-0.13.0.dist-info}/licenses/LICENSE +0 -0
operate/services/service.py
CHANGED
|
@@ -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 =
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
.generate_config_tendermint()
|
|
618
|
-
|
|
619
|
-
|
|
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
|
-
|
|
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(
|
|
680
|
+
self._build_docker(keys_manager=keys_manager, force=force, chain=chain)
|
|
669
681
|
if use_kubernetes:
|
|
670
|
-
self._build_kubernetes(
|
|
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
|
-
|
|
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(
|
|
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 /
|
|
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 /
|
|
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(
|
|
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 /
|
|
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 /
|
|
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
|
|
30
|
-
|
|
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 = "
|
|
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
|
|
58
|
-
|
|
59
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
105
|
-
"
|
|
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,
|
|
343
|
+
self, raise_if_inconsistent_owners: bool = True
|
|
110
344
|
) -> None:
|
|
111
|
-
"""
|
|
112
|
-
self.logger.info("[WALLET RECOVERY MANAGER]
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
145
|
-
|
|
146
|
-
|
|
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 =
|
|
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(
|
|
429
|
+
shutil.move(wallets_path, old_root)
|
|
200
430
|
for file in root.glob(f"{USER_JSON}*"):
|
|
201
|
-
shutil.move(
|
|
431
|
+
shutil.move(file, old_root / file.name)
|
|
202
432
|
|
|
203
|
-
shutil.
|
|
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.
|
|
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]
|
|
455
|
+
self.logger.info("[WALLET RECOVERY MANAGER] Complete recovery finished.")
|
|
File without changes
|