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.
- {olas_operate_middleware-0.10.19.dist-info → olas_operate_middleware-0.11.0.dist-info}/METADATA +3 -1
- {olas_operate_middleware-0.10.19.dist-info → olas_operate_middleware-0.11.0.dist-info}/RECORD +30 -27
- operate/bridge/bridge_manager.py +10 -12
- operate/bridge/providers/lifi_provider.py +5 -4
- operate/bridge/providers/native_bridge_provider.py +6 -5
- operate/bridge/providers/provider.py +22 -87
- operate/bridge/providers/relay_provider.py +5 -4
- operate/cli.py +446 -168
- operate/constants.py +22 -2
- operate/keys.py +13 -0
- operate/ledger/__init__.py +107 -2
- operate/ledger/profiles.py +79 -11
- operate/operate_types.py +205 -2
- operate/quickstart/run_service.py +6 -10
- operate/services/agent_runner.py +5 -3
- operate/services/deployment_runner.py +3 -0
- operate/services/funding_manager.py +904 -0
- operate/services/health_checker.py +4 -4
- operate/services/manage.py +183 -310
- operate/services/protocol.py +392 -140
- operate/services/service.py +81 -5
- operate/settings.py +70 -0
- operate/utils/__init__.py +0 -29
- operate/utils/gnosis.py +79 -24
- operate/utils/single_instance.py +226 -0
- operate/wallet/master.py +221 -181
- operate/wallet/wallet_recovery_manager.py +5 -5
- {olas_operate_middleware-0.10.19.dist-info → olas_operate_middleware-0.11.0.dist-info}/WHEEL +0 -0
- {olas_operate_middleware-0.10.19.dist-info → olas_operate_middleware-0.11.0.dist-info}/entry_points.txt +0 -0
- {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.
|
|
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
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
|
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.
|
|
160
|
-
"""Transfer
|
|
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
|
-
|
|
292
|
-
|
|
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=
|
|
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
|
-
|
|
312
|
-
|
|
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=
|
|
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
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
-
|
|
405
|
-
|
|
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
|
|
460
|
-
return self.
|
|
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=
|
|
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
|
|
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.
|
|
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
|
-
|
|
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=
|
|
517
|
+
amount=from_safe_amount,
|
|
493
518
|
chain=chain,
|
|
494
|
-
|
|
519
|
+
asset=asset,
|
|
520
|
+
from_safe=True,
|
|
495
521
|
rpc=rpc,
|
|
496
522
|
)
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
from_safe=
|
|
503
|
-
|
|
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 =
|
|
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.
|
|
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.
|
|
639
|
-
|
|
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:
|
|
662
|
-
raise ValueError(f"
|
|
663
|
-
safe = t.cast(str, self.safes[chain])
|
|
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
|
-
|
|
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
|
|
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
|