olas-operate-middleware 0.10.19__py3-none-any.whl → 0.11.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.
Files changed (30) hide show
  1. {olas_operate_middleware-0.10.19.dist-info → olas_operate_middleware-0.11.0.dist-info}/METADATA +3 -1
  2. {olas_operate_middleware-0.10.19.dist-info → olas_operate_middleware-0.11.0.dist-info}/RECORD +30 -27
  3. operate/bridge/bridge_manager.py +10 -12
  4. operate/bridge/providers/lifi_provider.py +5 -4
  5. operate/bridge/providers/native_bridge_provider.py +6 -5
  6. operate/bridge/providers/provider.py +22 -87
  7. operate/bridge/providers/relay_provider.py +5 -4
  8. operate/cli.py +446 -168
  9. operate/constants.py +22 -2
  10. operate/keys.py +13 -0
  11. operate/ledger/__init__.py +107 -2
  12. operate/ledger/profiles.py +79 -11
  13. operate/operate_types.py +205 -2
  14. operate/quickstart/run_service.py +6 -10
  15. operate/services/agent_runner.py +5 -3
  16. operate/services/deployment_runner.py +3 -0
  17. operate/services/funding_manager.py +904 -0
  18. operate/services/health_checker.py +4 -4
  19. operate/services/manage.py +183 -310
  20. operate/services/protocol.py +392 -140
  21. operate/services/service.py +81 -5
  22. operate/settings.py +70 -0
  23. operate/utils/__init__.py +0 -29
  24. operate/utils/gnosis.py +79 -24
  25. operate/utils/single_instance.py +226 -0
  26. operate/wallet/master.py +221 -181
  27. operate/wallet/wallet_recovery_manager.py +5 -5
  28. {olas_operate_middleware-0.10.19.dist-info → olas_operate_middleware-0.11.0.dist-info}/WHEEL +0 -0
  29. {olas_operate_middleware-0.10.19.dist-info → olas_operate_middleware-0.11.0.dist-info}/entry_points.txt +0 -0
  30. {olas_operate_middleware-0.10.19.dist-info → olas_operate_middleware-0.11.0.dist-info}/licenses/LICENSE +0 -0
operate/wallet/master.py CHANGED
@@ -20,16 +20,13 @@
20
20
  """Master key implementation"""
21
21
 
22
22
  import json
23
- import logging
24
23
  import os
25
24
  import typing as t
26
- from copy import deepcopy
27
25
  from dataclasses import dataclass, field
28
26
  from pathlib import Path
29
27
 
30
28
  from aea.crypto.base import Crypto, LedgerApi
31
- from aea.crypto.registries import make_ledger_api
32
- from aea_ledger_ethereum import DEFAULT_GAS_PRICE_STRATEGIES, EIP1559, GWEI, to_wei
29
+ from aea.helpers.logging import setup_logger
33
30
  from aea_ledger_ethereum.ethereum import EthereumApi, EthereumCrypto
34
31
  from autonomy.chain.base import registry_contracts
35
32
  from autonomy.chain.config import ChainType as ChainProfile
@@ -42,15 +39,20 @@ from operate.constants import (
42
39
  ON_CHAIN_INTERACT_TIMEOUT,
43
40
  ZERO_ADDRESS,
44
41
  )
45
- from operate.ledger import get_default_rpc
46
- from operate.ledger.profiles import ERC20_TOKENS, OLAS, USDC
47
- from operate.operate_types import Chain, LedgerType
42
+ from operate.ledger import (
43
+ get_default_ledger_api,
44
+ make_chain_ledger_api,
45
+ update_tx_with_gas_estimate,
46
+ update_tx_with_gas_pricing,
47
+ )
48
+ from operate.ledger.profiles import DUST, ERC20_TOKENS, format_asset_amount
49
+ from operate.operate_types import Chain, EncryptedData, LedgerType
48
50
  from operate.resource import LocalResource
49
51
  from operate.utils import create_backup
50
52
  from operate.utils.gnosis import add_owner
51
53
  from operate.utils.gnosis import create_safe as create_gnosis_safe
52
54
  from operate.utils.gnosis import (
53
- drain_eoa,
55
+ estimate_transfer_tx_fee,
54
56
  get_asset_balance,
55
57
  get_owners,
56
58
  remove_owner,
@@ -60,6 +62,9 @@ from operate.utils.gnosis import transfer as transfer_from_safe
60
62
  from operate.utils.gnosis import transfer_erc20_from_safe
61
63
 
62
64
 
65
+ logger = setup_logger(name="master_wallet")
66
+
67
+
63
68
  # TODO Organize exceptions definition
64
69
  class InsufficientFundsException(Exception):
65
70
  """Insufficient funds exception."""
@@ -77,6 +82,7 @@ class MasterWallet(LocalResource):
77
82
  safe_nonce: t.Optional[int] = None
78
83
 
79
84
  _key: str
85
+ _mnemonic: str
80
86
  _crypto: t.Optional[Crypto] = None
81
87
  _password: t.Optional[str] = None
82
88
  _crypto_cls: t.Type[Crypto]
@@ -105,59 +111,42 @@ class MasterWallet(LocalResource):
105
111
  """Key path."""
106
112
  return self.path / self._key
107
113
 
114
+ @property
115
+ def mnemonic_path(self) -> Path:
116
+ """Mnemonic path."""
117
+ return self.path / self._mnemonic
118
+
119
+ @staticmethod
108
120
  def ledger_api(
109
- self,
110
121
  chain: Chain,
111
122
  rpc: t.Optional[str] = None,
112
123
  ) -> LedgerApi:
113
124
  """Get ledger api object."""
114
- gas_price_strategies = deepcopy(DEFAULT_GAS_PRICE_STRATEGIES)
115
- if chain in (Chain.BASE, Chain.MODE, Chain.OPTIMISM):
116
- gas_price_strategies[EIP1559]["fallback_estimate"]["maxFeePerGas"] = to_wei(
117
- 5, GWEI
118
- )
119
-
120
- return make_ledger_api(
121
- self.ledger_type.name.lower(),
122
- address=(rpc or get_default_rpc(chain=chain)),
123
- chain_id=chain.id,
124
- gas_price_strategies=gas_price_strategies,
125
- )
126
-
127
- def transfer(
128
- self,
129
- to: str,
130
- amount: int,
131
- chain: Chain,
132
- from_safe: bool = True,
133
- rpc: t.Optional[str] = None,
134
- ) -> t.Optional[str]:
135
- """Transfer funds to the given account."""
136
- raise NotImplementedError()
125
+ if not rpc:
126
+ return get_default_ledger_api(chain=chain)
127
+ return make_chain_ledger_api(chain=chain, rpc=rpc)
137
128
 
138
- # pylint: disable=too-many-arguments
139
- def transfer_erc20(
129
+ def transfer( # pylint: disable=too-many-arguments
140
130
  self,
141
- token: str,
142
131
  to: str,
143
132
  amount: int,
144
133
  chain: Chain,
134
+ asset: str = ZERO_ADDRESS,
145
135
  from_safe: bool = True,
146
136
  rpc: t.Optional[str] = None,
147
137
  ) -> t.Optional[str]:
148
138
  """Transfer funds to the given account."""
149
139
  raise NotImplementedError()
150
140
 
151
- def transfer_asset(
141
+ def transfer_from_safe_then_eoa(
152
142
  self,
153
143
  to: str,
154
144
  amount: int,
155
145
  chain: Chain,
156
146
  asset: str = ZERO_ADDRESS,
157
- from_safe: bool = True,
158
147
  rpc: t.Optional[str] = None,
159
- ) -> t.Optional[str]:
160
- """Transfer erc20/native assets to the given account."""
148
+ ) -> t.List[str]:
149
+ """Transfer assets to the given account using Safe balance first, and EOA balance for leftover."""
161
150
  raise NotImplementedError()
162
151
 
163
152
  def drain(
@@ -175,6 +164,10 @@ class MasterWallet(LocalResource):
175
164
  """Create a new master wallet."""
176
165
  raise NotImplementedError()
177
166
 
167
+ def decrypt_mnemonic(self, password: str) -> t.Optional[t.List[str]]:
168
+ """Retrieve the mnemonic"""
169
+ raise NotImplementedError()
170
+
178
171
  def create_safe(
179
172
  self,
180
173
  chain: Chain,
@@ -213,6 +206,24 @@ class MasterWallet(LocalResource):
213
206
  """Updates password using the mnemonic."""
214
207
  raise NotImplementedError()
215
208
 
209
+ def get_balance(
210
+ self, chain: Chain, asset: str = ZERO_ADDRESS, from_safe: bool = True
211
+ ) -> int:
212
+ """Get wallet balance on a given chain."""
213
+ if from_safe:
214
+ if chain not in self.safes:
215
+ raise ValueError(f"Wallet does not have a Safe on chain {chain}.")
216
+
217
+ address = self.safes[chain]
218
+ else:
219
+ address = self.address
220
+
221
+ return get_asset_balance(
222
+ ledger_api=get_default_ledger_api(chain),
223
+ asset_address=asset,
224
+ address=address,
225
+ )
226
+
216
227
  # TODO move to resource.py if used in more resources similarly
217
228
  @property
218
229
  def extended_json(self) -> t.Dict:
@@ -239,12 +250,61 @@ class EthereumMasterWallet(MasterWallet):
239
250
 
240
251
  _file = ledger_type.config_file
241
252
  _key = ledger_type.key_file
253
+ _mnemonic = ledger_type.mnemonic_file
242
254
  _crypto_cls = EthereumCrypto
243
255
 
256
+ def _pre_transfer_checks(
257
+ self,
258
+ to: str,
259
+ amount: int,
260
+ chain: Chain,
261
+ from_safe: bool,
262
+ asset: str = ZERO_ADDRESS,
263
+ ) -> str:
264
+ """Checks conditions before transfer. Returns the to address checksummed."""
265
+ if amount <= 0:
266
+ raise ValueError(
267
+ "Transfer amount must be greater than zero, not transferring."
268
+ )
269
+
270
+ to = Web3().to_checksum_address(to)
271
+ if from_safe and chain not in self.safes:
272
+ raise ValueError(f"Wallet does not have a Safe on chain {chain}.")
273
+
274
+ balance = self.get_balance(chain=chain, asset=asset, from_safe=from_safe)
275
+ if balance < amount:
276
+ source = "Master Safe" if from_safe else " Master EOA"
277
+ source_address = self.safes[chain] if from_safe else self.address
278
+ raise InsufficientFundsException(
279
+ f"Cannot transfer {format_asset_amount(chain, asset, amount)} from {source} {source_address} to {to} on chain {chain.name}. "
280
+ f"Balance: {format_asset_amount(chain, asset, balance)}. Missing: {format_asset_amount(chain, asset, amount - balance)}."
281
+ )
282
+
283
+ return to
284
+
244
285
  def _transfer_from_eoa(
245
286
  self, to: str, amount: int, chain: Chain, rpc: t.Optional[str] = None
246
287
  ) -> t.Optional[str]:
247
288
  """Transfer funds from EOA wallet."""
289
+ balance = self.get_balance(chain=chain, from_safe=False)
290
+ tx_fee = estimate_transfer_tx_fee(
291
+ chain=chain, sender_address=self.address, to=to
292
+ )
293
+ if balance - tx_fee < amount <= balance:
294
+ # we assume that the user wants to drain the EOA
295
+ # we also account for dust here because withdraw call use some EOA balance to drain the safes first
296
+ amount = balance - tx_fee
297
+ if amount <= 0:
298
+ logger.warning(
299
+ f"Not enough balance to cover gas fees for transfer of {amount} on chain {chain} from EOA {self.address}. "
300
+ f"Balance is {balance}, estimated fee is {tx_fee}. Not transferring."
301
+ )
302
+ return None
303
+
304
+ to = self._pre_transfer_checks(
305
+ to=to, amount=amount, chain=chain, from_safe=False
306
+ )
307
+
248
308
  ledger_api = t.cast(EthereumApi, self.ledger_api(chain=chain, rpc=rpc))
249
309
  tx_helper = TxSettler(
250
310
  ledger_api=ledger_api,
@@ -288,13 +348,14 @@ class EthereumMasterWallet(MasterWallet):
288
348
  self, to: str, amount: int, chain: Chain, rpc: t.Optional[str] = None
289
349
  ) -> t.Optional[str]:
290
350
  """Transfer funds from safe wallet."""
291
- if self.safes is None:
292
- raise ValueError("Safes not initialized")
351
+ to = self._pre_transfer_checks(
352
+ to=to, amount=amount, chain=chain, from_safe=True
353
+ )
293
354
 
294
355
  return transfer_from_safe(
295
356
  ledger_api=self.ledger_api(chain=chain, rpc=rpc),
296
357
  crypto=self.crypto,
297
- safe=t.cast(str, self.safes[chain]),
358
+ safe=self.safes[chain],
298
359
  to=to,
299
360
  amount=amount,
300
361
  )
@@ -308,14 +369,15 @@ class EthereumMasterWallet(MasterWallet):
308
369
  rpc: t.Optional[str] = None,
309
370
  ) -> t.Optional[str]:
310
371
  """Transfer erc20 from safe wallet."""
311
- if self.safes is None:
312
- raise ValueError("Safes not initialized")
372
+ to = self._pre_transfer_checks(
373
+ to=to, amount=amount, chain=chain, from_safe=True, asset=token
374
+ )
313
375
 
314
376
  return transfer_erc20_from_safe(
315
377
  ledger_api=self.ledger_api(chain=chain, rpc=rpc),
316
378
  crypto=self.crypto,
317
379
  token=token,
318
- safe=t.cast(str, self.safes[chain]), # type: ignore
380
+ safe=self.safes[chain],
319
381
  to=to,
320
382
  amount=amount,
321
383
  )
@@ -329,6 +391,10 @@ class EthereumMasterWallet(MasterWallet):
329
391
  rpc: t.Optional[str] = None,
330
392
  ) -> t.Optional[str]:
331
393
  """Transfer erc20 from EOA wallet."""
394
+ to = self._pre_transfer_checks(
395
+ to=to, amount=amount, chain=chain, from_safe=False, asset=token
396
+ )
397
+
332
398
  wallet_address = self.address
333
399
  ledger_api = t.cast(EthereumApi, self.ledger_api(chain=chain, rpc=rpc))
334
400
  tx_settler = TxSettler(
@@ -357,10 +423,9 @@ class EthereumMasterWallet(MasterWallet):
357
423
  "nonce": ledger_api.api.eth.get_transaction_count(wallet_address),
358
424
  }
359
425
  )
360
- return ledger_api.update_with_gas_estimate(
361
- transaction=tx,
362
- raise_on_try=False,
363
- )
426
+ update_tx_with_gas_pricing(tx, ledger_api)
427
+ update_tx_with_gas_estimate(tx, ledger_api)
428
+ return tx
364
429
 
365
430
  setattr(tx_settler, "build", _build_transfer_tx) # noqa: B010
366
431
  tx_receipt = tx_settler.transact(
@@ -372,136 +437,109 @@ class EthereumMasterWallet(MasterWallet):
372
437
  tx_hash = tx_receipt.get("transactionHash", "").hex()
373
438
  return tx_hash
374
439
 
375
- def transfer(
440
+ def transfer( # pylint: disable=too-many-arguments
376
441
  self,
377
442
  to: str,
378
443
  amount: int,
379
444
  chain: Chain,
445
+ asset: str = ZERO_ADDRESS,
380
446
  from_safe: bool = True,
381
447
  rpc: t.Optional[str] = None,
382
448
  ) -> t.Optional[str]:
383
449
  """Transfer funds to the given account."""
384
- if amount <= 0:
385
- return None
386
-
387
450
  if from_safe:
388
- sender = t.cast(str, self.safes[chain])
389
- sender_str = f"Safe {sender}"
390
- else:
391
- sender = self.crypto.address
392
- sender_str = f"EOA {sender}"
393
-
394
- ledger_api = self.ledger_api(chain=chain, rpc=rpc)
395
- to = ledger_api.api.to_checksum_address(to)
396
- balance = ledger_api.get_balance(address=sender)
397
-
398
- if balance < amount:
399
- raise InsufficientFundsException(
400
- f"Cannot transfer {amount} native units from {sender_str} to {to} on chain {chain.value.capitalize()}. "
401
- f"Balance of {sender_str} is {balance} native units on chain {chain.value.capitalize()}."
402
- )
451
+ if asset == ZERO_ADDRESS:
452
+ return self._transfer_from_safe(
453
+ to=to,
454
+ amount=amount,
455
+ chain=chain,
456
+ rpc=rpc,
457
+ )
403
458
 
404
- if from_safe:
405
- return self._transfer_from_safe(
459
+ return self._transfer_erc20_from_safe(
460
+ token=asset,
406
461
  to=to,
407
462
  amount=amount,
408
463
  chain=chain,
409
464
  rpc=rpc,
410
465
  )
411
- return self._transfer_from_eoa(
412
- to=to,
413
- amount=amount,
414
- chain=chain,
415
- rpc=rpc,
416
- )
417
-
418
- # pylint: disable=too-many-arguments
419
- def transfer_erc20(
420
- self,
421
- token: str,
422
- to: str,
423
- amount: int,
424
- chain: Chain,
425
- from_safe: bool = True,
426
- rpc: t.Optional[str] = None,
427
- ) -> t.Optional[str]:
428
- """Transfer funds to the given account."""
429
- if amount <= 0:
430
- return None
431
-
432
- if from_safe:
433
- sender = t.cast(str, self.safes[chain])
434
- sender_str = f"Safe {sender}"
435
- else:
436
- sender = self.crypto.address
437
- sender_str = f"EOA {sender}"
438
-
439
- ledger_api = self.ledger_api(chain=chain, rpc=rpc)
440
- to = ledger_api.api.to_checksum_address(to)
441
- balance = (
442
- registry_contracts.erc20.get_instance(
443
- ledger_api=ledger_api,
444
- contract_address=token,
445
- )
446
- .functions.balanceOf(sender)
447
- .call()
448
- )
449
-
450
- tokens = {OLAS[chain]: "OLAS", USDC[chain]: "USDC"}
451
- token_name = tokens.get(token, token)
452
-
453
- if balance < amount:
454
- raise InsufficientFundsException(
455
- f"Cannot transfer {amount} {token_name} from {sender_str} to {to} on chain {chain.value.capitalize()}. "
456
- f"Balance of {sender_str} is {balance} {token_name} on chain {chain.value.capitalize()}."
457
- )
458
466
 
459
- if from_safe:
460
- return self._transfer_erc20_from_safe(
461
- token=token,
467
+ if asset == ZERO_ADDRESS:
468
+ return self._transfer_from_eoa(
462
469
  to=to,
463
470
  amount=amount,
464
471
  chain=chain,
465
472
  rpc=rpc,
466
473
  )
474
+
467
475
  return self._transfer_erc20_from_eoa(
468
- token=token,
476
+ token=asset,
469
477
  to=to,
470
478
  amount=amount,
471
479
  chain=chain,
472
480
  rpc=rpc,
473
481
  )
474
482
 
475
- def transfer_asset(
483
+ def transfer_from_safe_then_eoa(
476
484
  self,
477
485
  to: str,
478
486
  amount: int,
479
487
  chain: Chain,
480
488
  asset: str = ZERO_ADDRESS,
481
- from_safe: bool = True,
482
489
  rpc: t.Optional[str] = None,
483
- ) -> t.Optional[str]:
490
+ ) -> t.List[str]:
484
491
  """
485
- Transfer assets to the given account.
492
+ Transfer assets to the given account using Safe balance first, and EOA balance for leftover.
486
493
 
487
494
  If asset is a zero address, transfer native currency.
488
495
  """
496
+ safe_balance = self.get_balance(chain=chain, asset=asset, from_safe=True)
497
+ eoa_balance = self.get_balance(chain=chain, asset=asset, from_safe=False)
498
+ balance = safe_balance + eoa_balance
489
499
  if asset == ZERO_ADDRESS:
490
- return self.transfer(
500
+ # to account for gas fees burned in previous txs
501
+ # in this case we will set the amount = eoa_balance below
502
+ balance += DUST[chain]
503
+
504
+ if balance < amount:
505
+ raise InsufficientFundsException(
506
+ f"Cannot transfer {format_asset_amount(chain, asset, amount)} to {to} on chain {chain.name}. "
507
+ f"Balance of Master Safe {self.safes[chain]}: {format_asset_amount(chain, asset, safe_balance)}. "
508
+ f"Balance of Master EOA {self.address}: {format_asset_amount(chain, asset, eoa_balance)}. "
509
+ f"Missing: {format_asset_amount(chain, asset, amount - balance)}."
510
+ )
511
+
512
+ tx_hashes = []
513
+ from_safe_amount = min(safe_balance, amount)
514
+ if from_safe_amount > 0:
515
+ tx_hash = self.transfer(
491
516
  to=to,
492
- amount=amount,
517
+ amount=from_safe_amount,
493
518
  chain=chain,
494
- from_safe=from_safe,
519
+ asset=asset,
520
+ from_safe=True,
495
521
  rpc=rpc,
496
522
  )
497
- return self.transfer_erc20(
498
- token=asset,
499
- to=to,
500
- amount=amount,
501
- chain=chain,
502
- from_safe=from_safe,
503
- rpc=rpc,
504
- )
523
+ if tx_hash:
524
+ tx_hashes.append(tx_hash)
525
+ amount -= from_safe_amount
526
+
527
+ if amount > 0:
528
+ eoa_balance = self.get_balance(chain=chain, asset=asset, from_safe=False)
529
+ if (
530
+ asset == ZERO_ADDRESS
531
+ and eoa_balance <= amount <= eoa_balance + DUST[chain]
532
+ ):
533
+ # to make the internal function drain the EOA
534
+ amount = eoa_balance
535
+
536
+ tx_hash = self.transfer(
537
+ to=to, amount=amount, chain=chain, asset=asset, from_safe=False, rpc=rpc
538
+ )
539
+ if tx_hash:
540
+ tx_hashes.append(tx_hash)
541
+
542
+ return tx_hashes
505
543
 
506
544
  def drain(
507
545
  self,
@@ -511,23 +549,13 @@ class EthereumMasterWallet(MasterWallet):
511
549
  rpc: t.Optional[str] = None,
512
550
  ) -> None:
513
551
  """Drain all erc20/native assets to the given account."""
514
-
515
- ledger_api = self.ledger_api(chain=chain, rpc=rpc)
516
- assets = {token[chain] for token in ERC20_TOKENS}
517
-
518
- if from_safe:
519
- assets.add(ZERO_ADDRESS)
520
-
552
+ assets = [token[chain] for token in ERC20_TOKENS.values()] + [ZERO_ADDRESS]
521
553
  for asset in assets:
522
- balance = get_asset_balance(
523
- ledger_api=ledger_api,
524
- asset_address=asset,
525
- address=self.safes[chain] if from_safe else self.crypto.address,
526
- )
554
+ balance = self.get_balance(chain=chain, asset=asset, from_safe=from_safe)
527
555
  if balance <= 0:
528
556
  continue
529
557
 
530
- self.transfer_asset(
558
+ self.transfer(
531
559
  to=withdrawal_address,
532
560
  amount=balance,
533
561
  chain=chain,
@@ -536,14 +564,6 @@ class EthereumMasterWallet(MasterWallet):
536
564
  rpc=rpc,
537
565
  )
538
566
 
539
- if not from_safe:
540
- drain_eoa(
541
- ledger_api=ledger_api,
542
- crypto=self.crypto,
543
- withdrawal_address=withdrawal_address,
544
- chain_id=chain.id,
545
- )
546
-
547
567
  @classmethod
548
568
  def new(
549
569
  cls, password: str, path: Path
@@ -552,12 +572,26 @@ class EthereumMasterWallet(MasterWallet):
552
572
  # Backport support on aea
553
573
 
554
574
  eoa_wallet_path = path / cls._key
575
+ eoa_mnemonic_path = path / cls._mnemonic
576
+
555
577
  if eoa_wallet_path.exists():
556
578
  raise FileExistsError(f"Wallet file already exists at {eoa_wallet_path}.")
557
579
 
580
+ if eoa_mnemonic_path.exists():
581
+ raise FileExistsError(
582
+ f"Mnemonic file already exists at {eoa_mnemonic_path}."
583
+ )
584
+
585
+ eoa_wallet_path.parent.mkdir(parents=True, exist_ok=True)
586
+
587
+ # Store private key (Ethereum V3 keystore JSON) and encrypted mnemonic
558
588
  account = Account()
559
589
  account.enable_unaudited_hdwallet_features()
560
590
  crypto, mnemonic = account.create_with_mnemonic()
591
+ encrypted_mnemonic = EncryptedData.new(
592
+ path=eoa_mnemonic_path, password=password, plaintext_bytes=mnemonic.encode()
593
+ )
594
+ encrypted_mnemonic.store()
561
595
  eoa_wallet_path.write_text(
562
596
  data=json.dumps(
563
597
  Account.encrypt(
@@ -575,6 +609,17 @@ class EthereumMasterWallet(MasterWallet):
575
609
  wallet.password = password
576
610
  return wallet, mnemonic.split()
577
611
 
612
+ def decrypt_mnemonic(self, password: str) -> t.Optional[t.List[str]]:
613
+ """Retrieve the mnemonic"""
614
+ eoa_mnemonic_path = self.path / self.ledger_type.mnemonic_file
615
+
616
+ if not eoa_mnemonic_path.exists():
617
+ return None
618
+
619
+ encrypted_mnemonic = EncryptedData.load(eoa_mnemonic_path)
620
+ mnemonic = encrypted_mnemonic.decrypt(password).decode("utf-8")
621
+ return mnemonic.split()
622
+
578
623
  def update_password(self, new_password: str) -> None:
579
624
  """Updates password."""
580
625
  create_backup(self.path / self._key)
@@ -635,8 +680,9 @@ class EthereumMasterWallet(MasterWallet):
635
680
  rpc: t.Optional[str] = None,
636
681
  ) -> t.Optional[str]:
637
682
  """Create safe."""
638
- if chain in self.safe_chains:
639
- return None
683
+ if chain in self.safes:
684
+ raise ValueError(f"Wallet already has a Safe on chain {chain}.")
685
+
640
686
  safe, self.safe_nonce, tx_hash = create_gnosis_safe(
641
687
  ledger_api=self.ledger_api(chain=chain, rpc=rpc),
642
688
  crypto=self.crypto,
@@ -658,9 +704,9 @@ class EthereumMasterWallet(MasterWallet):
658
704
  ) -> bool:
659
705
  """Adds a backup owner if not present, or updates it by the provided backup owner. Setting a None backup owner will remove the current one, if any."""
660
706
  ledger_api = self.ledger_api(chain=chain, rpc=rpc)
661
- if chain not in self.safes: # type: ignore
662
- raise ValueError(f"Safes not created for chain {chain}!")
663
- safe = t.cast(str, self.safes[chain]) # type: ignore
707
+ if chain not in self.safes:
708
+ raise ValueError(f"Wallet does not have a Safe on chain {chain}.")
709
+ safe = t.cast(str, self.safes[chain])
664
710
  owners = get_owners(ledger_api=ledger_api, safe=safe)
665
711
 
666
712
  if len(owners) > 2:
@@ -715,39 +761,35 @@ class EthereumMasterWallet(MasterWallet):
715
761
  def extended_json(self) -> t.Dict:
716
762
  """Get JSON representation with extended information (e.g., safe owners)."""
717
763
  rpc = None
718
- tokens = (OLAS, USDC)
719
764
  wallet_json = self.json
720
765
 
721
- if not self.safes:
722
- return wallet_json
723
-
766
+ balances: t.Dict[str, t.Dict[str, t.Dict[str, int]]] = {}
724
767
  owner_sets = set()
725
768
  for chain, safe in self.safes.items():
769
+ chain_str = chain.value
726
770
  ledger_api = self.ledger_api(chain=chain, rpc=rpc)
727
771
  owners = get_owners(ledger_api=ledger_api, safe=safe)
728
772
  owners.remove(self.address)
729
773
 
730
- balances: t.Dict[str, int] = {}
731
- balances[ZERO_ADDRESS] = ledger_api.get_balance(safe) or 0
732
- for token in tokens:
733
- balance = (
734
- registry_contracts.erc20.get_instance(
735
- ledger_api=ledger_api,
736
- contract_address=token[chain],
737
- )
738
- .functions.balanceOf(safe)
739
- .call()
740
- )
741
- balances[token[chain]] = balance
774
+ balances[chain_str] = {self.address: {}, safe: {}}
742
775
 
776
+ assets = [token[chain] for token in ERC20_TOKENS.values()] + [ZERO_ADDRESS]
777
+ for asset in assets:
778
+ balances[chain_str][self.address][asset] = self.get_balance(
779
+ chain=chain, asset=asset, from_safe=False
780
+ )
781
+ balances[chain_str][safe][asset] = self.get_balance(
782
+ chain=chain, asset=asset, from_safe=True
783
+ )
743
784
  wallet_json["safes"][chain.value] = {
744
785
  wallet_json["safes"][chain.value]: {
745
786
  "backup_owners": owners,
746
- "balances": balances,
787
+ "balances": balances[chain_str][safe],
747
788
  }
748
789
  }
749
790
  owner_sets.add(frozenset(owners))
750
791
 
792
+ wallet_json["balances"] = balances
751
793
  wallet_json["extended_json"] = True
752
794
  wallet_json["consistent_safe_address"] = len(set(self.safes.values())) == 1
753
795
  wallet_json["consistent_backup_owner"] = len(owner_sets) == 1
@@ -847,12 +889,10 @@ class MasterWalletManager:
847
889
  def __init__(
848
890
  self,
849
891
  path: Path,
850
- logger: logging.Logger,
851
892
  password: t.Optional[str] = None,
852
893
  ) -> None:
853
894
  """Initialize master wallet manager."""
854
895
  self.path = path
855
- self.logger = logger
856
896
  self._password = password
857
897
 
858
898
  @property