olas-operate-middleware 0.12.2__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.2.dist-info → olas_operate_middleware-0.13.0.dist-info}/METADATA +1 -1
- {olas_operate_middleware-0.12.2.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 +43 -11
- operate/operate_types.py +16 -0
- operate/quickstart/reset_password.py +1 -2
- operate/quickstart/run_service.py +15 -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.2.dist-info → olas_operate_middleware-0.13.0.dist-info}/WHEEL +0 -0
- {olas_operate_middleware-0.12.2.dist-info → olas_operate_middleware-0.13.0.dist-info}/entry_points.txt +0 -0
- {olas_operate_middleware-0.12.2.dist-info → olas_operate_middleware-0.13.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -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
|
|
File without changes
|
|
File without changes
|