olas-operate-middleware 0.1.0rc59__py3-none-any.whl → 0.13.2__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.13.2.dist-info/METADATA +75 -0
- olas_operate_middleware-0.13.2.dist-info/RECORD +101 -0
- {olas_operate_middleware-0.1.0rc59.dist-info → olas_operate_middleware-0.13.2.dist-info}/WHEEL +1 -1
- operate/__init__.py +17 -0
- operate/account/user.py +35 -9
- operate/bridge/bridge_manager.py +470 -0
- operate/bridge/providers/lifi_provider.py +377 -0
- operate/bridge/providers/native_bridge_provider.py +677 -0
- operate/bridge/providers/provider.py +469 -0
- operate/bridge/providers/relay_provider.py +457 -0
- operate/cli.py +1565 -417
- operate/constants.py +60 -12
- operate/data/README.md +19 -0
- operate/data/contracts/{service_staking_token → dual_staking_token}/__init__.py +2 -2
- operate/data/contracts/dual_staking_token/build/DualStakingToken.json +443 -0
- operate/data/contracts/dual_staking_token/contract.py +132 -0
- operate/data/contracts/dual_staking_token/contract.yaml +23 -0
- operate/{ledger/base.py → data/contracts/foreign_omnibridge/__init__.py} +2 -19
- operate/data/contracts/foreign_omnibridge/build/ForeignOmnibridge.json +1372 -0
- operate/data/contracts/foreign_omnibridge/contract.py +130 -0
- operate/data/contracts/foreign_omnibridge/contract.yaml +23 -0
- operate/{ledger/solana.py → data/contracts/home_omnibridge/__init__.py} +2 -20
- operate/data/contracts/home_omnibridge/build/HomeOmnibridge.json +1421 -0
- operate/data/contracts/home_omnibridge/contract.py +80 -0
- operate/data/contracts/home_omnibridge/contract.yaml +23 -0
- operate/data/contracts/l1_standard_bridge/__init__.py +20 -0
- operate/data/contracts/l1_standard_bridge/build/L1StandardBridge.json +831 -0
- operate/data/contracts/l1_standard_bridge/contract.py +158 -0
- operate/data/contracts/l1_standard_bridge/contract.yaml +23 -0
- operate/data/contracts/l2_standard_bridge/__init__.py +20 -0
- operate/data/contracts/l2_standard_bridge/build/L2StandardBridge.json +626 -0
- operate/data/contracts/l2_standard_bridge/contract.py +130 -0
- operate/data/contracts/l2_standard_bridge/contract.yaml +23 -0
- operate/data/contracts/mech_activity/__init__.py +20 -0
- operate/data/contracts/mech_activity/build/MechActivity.json +111 -0
- operate/data/contracts/mech_activity/contract.py +44 -0
- operate/data/contracts/mech_activity/contract.yaml +23 -0
- operate/data/contracts/optimism_mintable_erc20/__init__.py +20 -0
- operate/data/contracts/optimism_mintable_erc20/build/OptimismMintableERC20.json +491 -0
- operate/data/contracts/optimism_mintable_erc20/contract.py +45 -0
- operate/data/contracts/optimism_mintable_erc20/contract.yaml +23 -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/data/contracts/requester_activity_checker/__init__.py +20 -0
- operate/data/contracts/requester_activity_checker/build/RequesterActivityChecker.json +111 -0
- operate/data/contracts/requester_activity_checker/contract.py +33 -0
- operate/data/contracts/requester_activity_checker/contract.yaml +23 -0
- operate/data/contracts/staking_token/__init__.py +20 -0
- operate/data/contracts/staking_token/build/StakingToken.json +1336 -0
- operate/data/contracts/{service_staking_token → staking_token}/contract.py +27 -13
- operate/data/contracts/staking_token/contract.yaml +23 -0
- operate/data/contracts/uniswap_v2_erc20/contract.yaml +3 -1
- operate/data/contracts/uniswap_v2_erc20/tests/__init__.py +20 -0
- operate/data/contracts/uniswap_v2_erc20/tests/test_contract.py +363 -0
- operate/keys.py +118 -33
- operate/ledger/__init__.py +159 -56
- operate/ledger/profiles.py +321 -18
- operate/migration.py +555 -0
- operate/{http → operate_http}/__init__.py +3 -2
- operate/{http → operate_http}/exceptions.py +6 -4
- operate/operate_types.py +544 -0
- operate/pearl.py +13 -1
- operate/quickstart/analyse_logs.py +118 -0
- operate/quickstart/claim_staking_rewards.py +104 -0
- operate/quickstart/reset_configs.py +106 -0
- operate/quickstart/reset_password.py +70 -0
- operate/quickstart/reset_staking.py +145 -0
- operate/quickstart/run_service.py +726 -0
- operate/quickstart/stop_service.py +72 -0
- operate/quickstart/terminate_on_chain_service.py +83 -0
- operate/quickstart/utils.py +298 -0
- operate/resource.py +62 -3
- operate/services/agent_runner.py +202 -0
- operate/services/deployment_runner.py +868 -0
- operate/services/funding_manager.py +929 -0
- operate/services/health_checker.py +280 -0
- operate/services/manage.py +2356 -620
- operate/services/protocol.py +1246 -340
- operate/services/service.py +756 -391
- operate/services/utils/mech.py +103 -0
- operate/services/utils/tendermint.py +86 -12
- operate/settings.py +70 -0
- operate/utils/__init__.py +135 -0
- operate/utils/gnosis.py +407 -80
- operate/utils/single_instance.py +226 -0
- operate/utils/ssl.py +133 -0
- operate/wallet/master.py +708 -123
- operate/wallet/wallet_recovery_manager.py +507 -0
- olas_operate_middleware-0.1.0rc59.dist-info/METADATA +0 -304
- olas_operate_middleware-0.1.0rc59.dist-info/RECORD +0 -41
- operate/data/contracts/service_staking_token/build/ServiceStakingToken.json +0 -1273
- operate/data/contracts/service_staking_token/contract.yaml +0 -23
- operate/ledger/ethereum.py +0 -48
- operate/types.py +0 -260
- {olas_operate_middleware-0.1.0rc59.dist-info → olas_operate_middleware-0.13.2.dist-info}/entry_points.txt +0 -0
- {olas_operate_middleware-0.1.0rc59.dist-info → olas_operate_middleware-0.13.2.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,507 @@
|
|
|
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 enum
|
|
23
|
+
import shutil
|
|
24
|
+
import typing as t
|
|
25
|
+
import uuid
|
|
26
|
+
from dataclasses import dataclass, field
|
|
27
|
+
from logging import Logger
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
from operate.account.user import UserAccount
|
|
31
|
+
from operate.constants import (
|
|
32
|
+
KEYS_DIR,
|
|
33
|
+
MSG_INVALID_PASSWORD,
|
|
34
|
+
USER_JSON,
|
|
35
|
+
WALLETS_DIR,
|
|
36
|
+
ZERO_ADDRESS,
|
|
37
|
+
)
|
|
38
|
+
from operate.keys import KeysManager
|
|
39
|
+
from operate.ledger import get_default_ledger_api
|
|
40
|
+
from operate.ledger.profiles import DEFAULT_RECOVERY_TOPUPS
|
|
41
|
+
from operate.operate_types import ChainAmounts
|
|
42
|
+
from operate.resource import LocalResource
|
|
43
|
+
from operate.services.manage import ServiceManager
|
|
44
|
+
from operate.utils.gnosis import get_asset_balance, get_owners
|
|
45
|
+
from operate.wallet.master import MasterWalletManager
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
RECOVERY_BUNDLE_PREFIX = "eb-"
|
|
49
|
+
RECOVERY_NEW_OBJECTS_DIR = "new"
|
|
50
|
+
RECOVERY_OLD_OBJECTS_DIR = "old"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class WalletRecoveryStatus(str, enum.Enum):
|
|
54
|
+
"""ProviderRequestStatus"""
|
|
55
|
+
|
|
56
|
+
NOT_PREPARED = "NOT_PREPARED"
|
|
57
|
+
PREPARED = "PREPARED"
|
|
58
|
+
IN_PROGRESS = "IN_PROGRESS"
|
|
59
|
+
COMPLETED = "COMPLETED"
|
|
60
|
+
|
|
61
|
+
def __str__(self) -> str:
|
|
62
|
+
"""__str__"""
|
|
63
|
+
return self.value
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class WalletRecoveryError(Exception):
|
|
67
|
+
"""WalletRecoveryError"""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class WalletRecoveryManagerData(LocalResource):
|
|
72
|
+
"""BridgeManagerData"""
|
|
73
|
+
|
|
74
|
+
path: Path
|
|
75
|
+
version: int = 1
|
|
76
|
+
last_prepared_bundle_id: t.Optional[str] = None
|
|
77
|
+
new_agent_keys: t.Dict[str, t.Dict[str, str]] = field(default_factory=dict)
|
|
78
|
+
|
|
79
|
+
_file = "wallet_recovery.json"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class WalletRecoveryManager:
|
|
83
|
+
"""WalletRecoveryManager"""
|
|
84
|
+
|
|
85
|
+
def __init__(
|
|
86
|
+
self,
|
|
87
|
+
path: Path,
|
|
88
|
+
logger: Logger,
|
|
89
|
+
wallet_manager: MasterWalletManager,
|
|
90
|
+
service_manager: ServiceManager,
|
|
91
|
+
) -> None:
|
|
92
|
+
"""Initialize wallet recovery manager."""
|
|
93
|
+
self.path = path
|
|
94
|
+
self.logger = logger
|
|
95
|
+
self.wallet_manager = wallet_manager
|
|
96
|
+
self.service_manager = service_manager
|
|
97
|
+
|
|
98
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
99
|
+
file = path / WalletRecoveryManagerData._file
|
|
100
|
+
if not file.exists():
|
|
101
|
+
WalletRecoveryManagerData(path=path).store()
|
|
102
|
+
|
|
103
|
+
self.data: WalletRecoveryManagerData = t.cast(
|
|
104
|
+
WalletRecoveryManagerData, WalletRecoveryManagerData.load(path)
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
def prepare_recovery( # pylint: disable=too-many-locals
|
|
108
|
+
self, new_password: str
|
|
109
|
+
) -> t.Dict:
|
|
110
|
+
"""Prepare recovery"""
|
|
111
|
+
self.logger.info("[WALLET RECOVERY MANAGER] Prepare recovery started.")
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
_ = self.wallet_manager.password
|
|
115
|
+
except ValueError:
|
|
116
|
+
pass
|
|
117
|
+
else:
|
|
118
|
+
raise WalletRecoveryError(
|
|
119
|
+
"Wallet recovery cannot be executed while logged in."
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
if not new_password:
|
|
123
|
+
raise ValueError("'new_password' must be a non-empty string.")
|
|
124
|
+
|
|
125
|
+
for wallet in self.wallet_manager:
|
|
126
|
+
for chain, safe in wallet.safes.items():
|
|
127
|
+
ledger_api = get_default_ledger_api(chain)
|
|
128
|
+
owners = get_owners(ledger_api=ledger_api, safe=safe)
|
|
129
|
+
|
|
130
|
+
if wallet.address not in owners:
|
|
131
|
+
self.logger.warning(
|
|
132
|
+
f"Wallet {wallet.address} is not an owner of Safe {safe} on {chain.value}. (Interrupted swapping of Safe owners?)"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
backup_owners = set(owners) - {wallet.address}
|
|
136
|
+
if len(backup_owners) < 1:
|
|
137
|
+
raise WalletRecoveryError(
|
|
138
|
+
f"Safe {safe} on {chain.value} has less than 1 backup owner."
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
last_prepared_bundle_id = self.data.last_prepared_bundle_id
|
|
142
|
+
if (
|
|
143
|
+
last_prepared_bundle_id is not None
|
|
144
|
+
and self.status()["num_safes_with_new_wallet"] > 0
|
|
145
|
+
):
|
|
146
|
+
self.logger.info(
|
|
147
|
+
f"[WALLET RECOVERY MANAGER] Uncompleted bundle {last_prepared_bundle_id} has Safes with new wallet."
|
|
148
|
+
)
|
|
149
|
+
return self._load_bundle(
|
|
150
|
+
bundle_id=last_prepared_bundle_id, new_password=new_password
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Create new recovery bundle
|
|
154
|
+
bundle_id = f"{RECOVERY_BUNDLE_PREFIX}{str(uuid.uuid4())}"
|
|
155
|
+
new_root = self.path / bundle_id / RECOVERY_NEW_OBJECTS_DIR
|
|
156
|
+
new_root.mkdir(parents=True, exist_ok=False)
|
|
157
|
+
UserAccount.new(new_password, new_root / USER_JSON)
|
|
158
|
+
|
|
159
|
+
new_wallets_path = new_root / WALLETS_DIR
|
|
160
|
+
new_wallet_manager = MasterWalletManager(
|
|
161
|
+
path=new_wallets_path, password=new_password
|
|
162
|
+
)
|
|
163
|
+
new_wallet_manager.setup()
|
|
164
|
+
|
|
165
|
+
for wallet in self.wallet_manager:
|
|
166
|
+
ledger_type = wallet.ledger_type
|
|
167
|
+
new_wallet, _ = new_wallet_manager.create(ledger_type=ledger_type)
|
|
168
|
+
self.logger.info(
|
|
169
|
+
f"[WALLET RECOVERY MANAGER] Created new wallet {ledger_type=} {new_wallet.address=}"
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
new_keys_manager = KeysManager(
|
|
173
|
+
path=new_root / KEYS_DIR, password=new_password, logger=self.logger
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
new_agent_keys = self.data.new_agent_keys
|
|
177
|
+
for service in self.service_manager.get_all_services()[0]:
|
|
178
|
+
service_config_id = service.service_config_id
|
|
179
|
+
new_agent_keys.setdefault(service_config_id, {})
|
|
180
|
+
for agent_address in service.agent_addresses:
|
|
181
|
+
new_agent_address = new_keys_manager.create()
|
|
182
|
+
new_agent_keys[service_config_id][agent_address] = new_agent_address
|
|
183
|
+
|
|
184
|
+
self.data.last_prepared_bundle_id = bundle_id
|
|
185
|
+
self.data.store()
|
|
186
|
+
self.logger.info(
|
|
187
|
+
"[WALLET RECOVERY MANAGER] Prepare recovery finished with new bundle."
|
|
188
|
+
)
|
|
189
|
+
return self._load_bundle(bundle_id=bundle_id, new_password=new_password)
|
|
190
|
+
|
|
191
|
+
def _load_bundle( # pylint: disable=too-many-locals
|
|
192
|
+
self, bundle_id: str, new_password: t.Optional[str] = None
|
|
193
|
+
) -> t.Dict:
|
|
194
|
+
new_root = self.path / bundle_id / RECOVERY_NEW_OBJECTS_DIR
|
|
195
|
+
|
|
196
|
+
new_user_account = UserAccount.load(new_root / USER_JSON)
|
|
197
|
+
if new_password is not None and not new_user_account.is_valid(
|
|
198
|
+
password=new_password
|
|
199
|
+
):
|
|
200
|
+
raise ValueError(MSG_INVALID_PASSWORD)
|
|
201
|
+
|
|
202
|
+
new_wallets_path = new_root / WALLETS_DIR
|
|
203
|
+
new_wallet_manager = MasterWalletManager(
|
|
204
|
+
path=new_wallets_path, password=new_password
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
num_safes = 0
|
|
208
|
+
num_safes_with_new_wallet = 0
|
|
209
|
+
num_safes_with_old_wallet = 0
|
|
210
|
+
num_safes_with_both_wallets = 0
|
|
211
|
+
backup_owner_sets = set()
|
|
212
|
+
|
|
213
|
+
wallets = []
|
|
214
|
+
for wallet in self.wallet_manager:
|
|
215
|
+
new_wallet = next(
|
|
216
|
+
(w for w in new_wallet_manager if w.ledger_type == wallet.ledger_type)
|
|
217
|
+
)
|
|
218
|
+
new_mnemonic = None
|
|
219
|
+
if new_password:
|
|
220
|
+
new_mnemonic = new_wallet.decrypt_mnemonic(password=new_password)
|
|
221
|
+
|
|
222
|
+
wallet_json = wallet.json
|
|
223
|
+
|
|
224
|
+
for chain, safe in wallet.safes.items():
|
|
225
|
+
chain_str = chain.value
|
|
226
|
+
ledger_api = get_default_ledger_api(chain)
|
|
227
|
+
owners = get_owners(ledger_api=ledger_api, safe=safe)
|
|
228
|
+
backup_owners = list(set(owners) - {wallet.address, new_wallet.address})
|
|
229
|
+
backup_owner_sets.add(frozenset(backup_owners))
|
|
230
|
+
|
|
231
|
+
num_safes += 1
|
|
232
|
+
if new_wallet.address in owners and wallet.address in owners:
|
|
233
|
+
num_safes_with_both_wallets += 1
|
|
234
|
+
if new_wallet.address in owners:
|
|
235
|
+
num_safes_with_new_wallet += 1
|
|
236
|
+
if wallet.address in owners:
|
|
237
|
+
num_safes_with_old_wallet += 1
|
|
238
|
+
|
|
239
|
+
wallet_json["safes"][chain_str] = {
|
|
240
|
+
safe: {
|
|
241
|
+
"owners": owners,
|
|
242
|
+
"backup_owners": backup_owners,
|
|
243
|
+
"owner_to_remove": wallet.address
|
|
244
|
+
if wallet.address in owners
|
|
245
|
+
else None,
|
|
246
|
+
"owner_to_add": new_wallet.address
|
|
247
|
+
if new_wallet.address not in owners
|
|
248
|
+
else None,
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
wallets.append(
|
|
253
|
+
{
|
|
254
|
+
"current_wallet": wallet_json,
|
|
255
|
+
"new_wallet": new_wallet.json,
|
|
256
|
+
"new_mnemonic": new_mnemonic,
|
|
257
|
+
}
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
if num_safes_with_new_wallet == 0:
|
|
261
|
+
status = WalletRecoveryStatus.PREPARED
|
|
262
|
+
elif num_safes_with_new_wallet < num_safes:
|
|
263
|
+
status = WalletRecoveryStatus.IN_PROGRESS
|
|
264
|
+
else:
|
|
265
|
+
status = WalletRecoveryStatus.COMPLETED
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
"id": bundle_id,
|
|
269
|
+
"wallets": wallets,
|
|
270
|
+
"status": status,
|
|
271
|
+
"all_safes_have_backup_owner": all(
|
|
272
|
+
len(owners) >= 1 for owners in backup_owner_sets
|
|
273
|
+
),
|
|
274
|
+
"consistent_backup_owner": len(backup_owner_sets) == 1,
|
|
275
|
+
"consistent_backup_owner_count": all(
|
|
276
|
+
len(owners) == 1 for owners in backup_owner_sets
|
|
277
|
+
),
|
|
278
|
+
"prepared": bundle_id is not None,
|
|
279
|
+
"has_swaps": num_safes_with_new_wallet > 0,
|
|
280
|
+
"has_pending_swaps": num_safes_with_new_wallet < num_safes,
|
|
281
|
+
"num_safes": num_safes,
|
|
282
|
+
"num_safes_with_new_wallet": num_safes_with_new_wallet,
|
|
283
|
+
"num_safes_with_old_wallet": num_safes_with_old_wallet,
|
|
284
|
+
"num_safes_with_both_wallets": num_safes_with_both_wallets,
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
def recovery_requirements( # pylint: disable=too-many-locals
|
|
288
|
+
self,
|
|
289
|
+
) -> t.Dict[str, t.Any]:
|
|
290
|
+
"""Get recovery funding requirements for backup owners."""
|
|
291
|
+
|
|
292
|
+
bundle_id = self.data.last_prepared_bundle_id
|
|
293
|
+
if not bundle_id:
|
|
294
|
+
return {}
|
|
295
|
+
|
|
296
|
+
balances = ChainAmounts()
|
|
297
|
+
requirements = ChainAmounts()
|
|
298
|
+
pending_backup_owner_swaps: t.Dict = {}
|
|
299
|
+
|
|
300
|
+
new_root = self.path / bundle_id / RECOVERY_NEW_OBJECTS_DIR
|
|
301
|
+
new_wallets_path = new_root / WALLETS_DIR
|
|
302
|
+
new_wallet_manager = MasterWalletManager(path=new_wallets_path, password=None)
|
|
303
|
+
|
|
304
|
+
for wallet in self.wallet_manager:
|
|
305
|
+
new_wallet = next(
|
|
306
|
+
(w for w in new_wallet_manager if w.ledger_type == wallet.ledger_type)
|
|
307
|
+
)
|
|
308
|
+
for chain, safe in wallet.safes.items():
|
|
309
|
+
chain_str = chain.value
|
|
310
|
+
balances.setdefault(chain_str, {})
|
|
311
|
+
requirements.setdefault(chain_str, {})
|
|
312
|
+
|
|
313
|
+
ledger_api = get_default_ledger_api(chain)
|
|
314
|
+
owners = get_owners(ledger_api=ledger_api, safe=safe)
|
|
315
|
+
backup_owners = set(owners) - {wallet.address} - {new_wallet.address}
|
|
316
|
+
|
|
317
|
+
if len(backup_owners) != 1:
|
|
318
|
+
self.logger.warning(
|
|
319
|
+
f"[WALLET RECOVERY MANAGER] Safe {safe} on {chain.value} has unexpected number of backup owners: {len(backup_owners)}."
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
for backup_owner in backup_owners:
|
|
323
|
+
balances[chain_str].setdefault(backup_owner, {})
|
|
324
|
+
balances[chain_str][backup_owner][ZERO_ADDRESS] = get_asset_balance(
|
|
325
|
+
ledger_api=ledger_api,
|
|
326
|
+
asset_address=ZERO_ADDRESS,
|
|
327
|
+
address=backup_owner,
|
|
328
|
+
raise_on_invalid_address=False,
|
|
329
|
+
)
|
|
330
|
+
requirements[chain_str].setdefault(backup_owner, {}).setdefault(
|
|
331
|
+
ZERO_ADDRESS, 0
|
|
332
|
+
)
|
|
333
|
+
if new_wallet.address not in owners:
|
|
334
|
+
requirements[chain_str][backup_owner][
|
|
335
|
+
ZERO_ADDRESS
|
|
336
|
+
] += DEFAULT_RECOVERY_TOPUPS[chain][ZERO_ADDRESS]
|
|
337
|
+
pending_backup_owner_swaps.setdefault(chain_str, [])
|
|
338
|
+
if safe not in pending_backup_owner_swaps[chain_str]:
|
|
339
|
+
pending_backup_owner_swaps[chain_str].append(safe)
|
|
340
|
+
|
|
341
|
+
refill_requirements = ChainAmounts.shortfalls(
|
|
342
|
+
requirements=requirements, balances=balances
|
|
343
|
+
)
|
|
344
|
+
is_refill_required = any(
|
|
345
|
+
amount > 0
|
|
346
|
+
for address in refill_requirements.values()
|
|
347
|
+
for assets in address.values()
|
|
348
|
+
for amount in assets.values()
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
"balances": balances,
|
|
353
|
+
"total_requirements": requirements,
|
|
354
|
+
"refill_requirements": refill_requirements,
|
|
355
|
+
"is_refill_required": is_refill_required,
|
|
356
|
+
"pending_backup_owner_swaps": pending_backup_owner_swaps,
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
def status(self) -> t.Dict[str, t.Any]:
|
|
360
|
+
"""Get recovery status."""
|
|
361
|
+
bundle_id = self.data.last_prepared_bundle_id
|
|
362
|
+
if bundle_id is None:
|
|
363
|
+
backup_owner_sets = set()
|
|
364
|
+
for wallet in self.wallet_manager:
|
|
365
|
+
for chain, safe in wallet.safes.items():
|
|
366
|
+
ledger_api = get_default_ledger_api(chain)
|
|
367
|
+
owners = get_owners(ledger_api=ledger_api, safe=safe)
|
|
368
|
+
backup_owners = list(set(owners) - {wallet.address})
|
|
369
|
+
backup_owner_sets.add(frozenset(backup_owners))
|
|
370
|
+
|
|
371
|
+
return {
|
|
372
|
+
"id": None,
|
|
373
|
+
"wallets": [],
|
|
374
|
+
"status": WalletRecoveryStatus.NOT_PREPARED,
|
|
375
|
+
"all_safes_have_backup_owner": all(
|
|
376
|
+
len(owners) >= 1 for owners in backup_owner_sets
|
|
377
|
+
),
|
|
378
|
+
"consistent_backup_owner": len(backup_owner_sets) == 1,
|
|
379
|
+
"consistent_backup_owner_count": all(
|
|
380
|
+
len(owners) == 1 for owners in backup_owner_sets
|
|
381
|
+
),
|
|
382
|
+
"prepared": False,
|
|
383
|
+
"bundle_id": bundle_id,
|
|
384
|
+
"has_swaps": False,
|
|
385
|
+
"has_pending_swaps": False,
|
|
386
|
+
"num_safes": 0,
|
|
387
|
+
"num_safes_with_new_wallet": 0,
|
|
388
|
+
"num_safes_with_old_wallet": 0,
|
|
389
|
+
"num_safes_with_both_wallets": 0,
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return self._load_bundle(bundle_id=bundle_id)
|
|
393
|
+
|
|
394
|
+
def complete_recovery( # pylint: disable=too-many-locals,too-many-statements
|
|
395
|
+
self, raise_if_inconsistent_owners: bool = True
|
|
396
|
+
) -> None:
|
|
397
|
+
"""Complete recovery"""
|
|
398
|
+
self.logger.info("[WALLET RECOVERY MANAGER] Complete recovery started.")
|
|
399
|
+
|
|
400
|
+
def _report_issue(msg: str) -> None:
|
|
401
|
+
self.logger.warning(f"[WALLET RECOVERY MANAGER] {msg}")
|
|
402
|
+
if raise_if_inconsistent_owners:
|
|
403
|
+
raise WalletRecoveryError(f"{msg}")
|
|
404
|
+
|
|
405
|
+
try:
|
|
406
|
+
_ = self.wallet_manager.password
|
|
407
|
+
except ValueError:
|
|
408
|
+
pass
|
|
409
|
+
else:
|
|
410
|
+
raise WalletRecoveryError(
|
|
411
|
+
"Wallet recovery cannot be executed while logged in."
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
bundle_id = self.data.last_prepared_bundle_id
|
|
415
|
+
|
|
416
|
+
if not bundle_id:
|
|
417
|
+
raise WalletRecoveryError("No prepared bundle found.")
|
|
418
|
+
|
|
419
|
+
root = self.path.parent # .operate root
|
|
420
|
+
wallets_path = root / WALLETS_DIR
|
|
421
|
+
new_root = self.path / bundle_id / RECOVERY_NEW_OBJECTS_DIR
|
|
422
|
+
new_wallets_path = new_root / WALLETS_DIR
|
|
423
|
+
new_keys_path = new_root / KEYS_DIR
|
|
424
|
+
old_root = self.path / bundle_id / RECOVERY_OLD_OBJECTS_DIR
|
|
425
|
+
|
|
426
|
+
if not new_root.exists() or not new_root.is_dir():
|
|
427
|
+
raise RuntimeError(f"Recovery bundle {bundle_id} does not exist.")
|
|
428
|
+
|
|
429
|
+
if old_root.exists() and old_root.is_dir():
|
|
430
|
+
raise RuntimeError(
|
|
431
|
+
f"Recovery bundle {bundle_id} has been executed already."
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
new_wallet_manager = MasterWalletManager(path=new_wallets_path, password=None)
|
|
435
|
+
|
|
436
|
+
ledger_types = {item.ledger_type for item in self.wallet_manager}
|
|
437
|
+
new_ledger_types = {item.ledger_type for item in new_wallet_manager}
|
|
438
|
+
|
|
439
|
+
if ledger_types != new_ledger_types:
|
|
440
|
+
raise WalletRecoveryError(
|
|
441
|
+
f"Ledger type mismatch: {ledger_types=}, {new_ledger_types=}."
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
for wallet in self.wallet_manager:
|
|
445
|
+
new_wallet = next(
|
|
446
|
+
(w for w in new_wallet_manager if w.ledger_type == wallet.ledger_type)
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
all_backup_owners = set()
|
|
450
|
+
for chain, safe in wallet.safes.items():
|
|
451
|
+
ledger_api = get_default_ledger_api(chain)
|
|
452
|
+
owners = get_owners(ledger_api=ledger_api, safe=safe)
|
|
453
|
+
if new_wallet.address not in owners:
|
|
454
|
+
raise WalletRecoveryError(
|
|
455
|
+
f"Incorrect owners. Wallet {new_wallet.address} is not an owner of Safe {safe} on {chain}."
|
|
456
|
+
)
|
|
457
|
+
if wallet.address in owners:
|
|
458
|
+
_report_issue(
|
|
459
|
+
f"Inconsistent owners. Current wallet {wallet.address} is still an owner of Safe {safe} on {chain}."
|
|
460
|
+
)
|
|
461
|
+
if len(owners) != 2:
|
|
462
|
+
_report_issue(
|
|
463
|
+
f"Inconsistent owners. Safe {safe} on {chain} has {len(owners)} != 2 owners."
|
|
464
|
+
)
|
|
465
|
+
all_backup_owners.update(set(owners) - {new_wallet.address})
|
|
466
|
+
|
|
467
|
+
if len(all_backup_owners) != 1:
|
|
468
|
+
_report_issue(
|
|
469
|
+
f"Inconsistent owners. Backup owners differ across Safes on chains {', '.join(chain.value for chain in wallet.safes.keys())}. "
|
|
470
|
+
f"Found backup owners: {', '.join(map(str, all_backup_owners))}."
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
new_wallet.safes = wallet.safes.copy()
|
|
474
|
+
new_wallet.safe_chains = wallet.safe_chains.copy()
|
|
475
|
+
new_wallet.safe_nonce = wallet.safe_nonce
|
|
476
|
+
new_wallet.store()
|
|
477
|
+
|
|
478
|
+
# Update configuration recovery
|
|
479
|
+
try:
|
|
480
|
+
old_root.mkdir(parents=True, exist_ok=False)
|
|
481
|
+
shutil.move(wallets_path, old_root)
|
|
482
|
+
for file in root.glob(f"{USER_JSON}*"):
|
|
483
|
+
shutil.move(file, old_root / file.name)
|
|
484
|
+
|
|
485
|
+
shutil.copytree(
|
|
486
|
+
new_wallets_path, root / new_wallets_path.name, dirs_exist_ok=True
|
|
487
|
+
)
|
|
488
|
+
for file in new_root.glob(f"{USER_JSON}*"):
|
|
489
|
+
shutil.copy2(file, root / file.name)
|
|
490
|
+
for file in new_keys_path.iterdir():
|
|
491
|
+
shutil.copy2(file, root / KEYS_DIR / file.name)
|
|
492
|
+
|
|
493
|
+
new_agent_keys = self.data.new_agent_keys
|
|
494
|
+
for service in self.service_manager.get_all_services()[0]:
|
|
495
|
+
service_config_id = service.service_config_id
|
|
496
|
+
service.agent_addresses = [
|
|
497
|
+
new_agent_keys[service_config_id][addr]
|
|
498
|
+
for addr in service.agent_addresses
|
|
499
|
+
]
|
|
500
|
+
service.store()
|
|
501
|
+
|
|
502
|
+
self.data.last_prepared_bundle_id = None
|
|
503
|
+
self.data.store()
|
|
504
|
+
except Exception as e:
|
|
505
|
+
raise RuntimeError from e
|
|
506
|
+
|
|
507
|
+
self.logger.info("[WALLET RECOVERY MANAGER] Complete recovery finished.")
|