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
operate/wallet/master.py
CHANGED
|
@@ -20,39 +20,69 @@
|
|
|
20
20
|
"""Master key implementation"""
|
|
21
21
|
|
|
22
22
|
import json
|
|
23
|
+
import os
|
|
23
24
|
import typing as t
|
|
24
|
-
from dataclasses import dataclass
|
|
25
|
+
from dataclasses import dataclass, field
|
|
25
26
|
from pathlib import Path
|
|
26
27
|
|
|
27
28
|
from aea.crypto.base import Crypto, LedgerApi
|
|
28
|
-
from aea.
|
|
29
|
+
from aea.helpers.logging import setup_logger
|
|
29
30
|
from aea_ledger_ethereum.ethereum import EthereumApi, EthereumCrypto
|
|
31
|
+
from autonomy.chain.base import registry_contracts
|
|
30
32
|
from autonomy.chain.config import ChainType as ChainProfile
|
|
31
33
|
from autonomy.chain.tx import TxSettler
|
|
32
|
-
from web3 import Account
|
|
34
|
+
from web3 import Account, Web3
|
|
33
35
|
|
|
34
36
|
from operate.constants import (
|
|
35
37
|
ON_CHAIN_INTERACT_RETRIES,
|
|
36
38
|
ON_CHAIN_INTERACT_SLEEP,
|
|
37
39
|
ON_CHAIN_INTERACT_TIMEOUT,
|
|
40
|
+
ZERO_ADDRESS,
|
|
38
41
|
)
|
|
39
|
-
from operate.ledger import
|
|
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
|
|
40
50
|
from operate.resource import LocalResource
|
|
41
|
-
from operate.
|
|
51
|
+
from operate.utils import create_backup
|
|
42
52
|
from operate.utils.gnosis import add_owner
|
|
43
53
|
from operate.utils.gnosis import create_safe as create_gnosis_safe
|
|
44
|
-
from operate.utils.gnosis import
|
|
54
|
+
from operate.utils.gnosis import (
|
|
55
|
+
estimate_transfer_tx_fee,
|
|
56
|
+
get_asset_balance,
|
|
57
|
+
get_owners,
|
|
58
|
+
remove_owner,
|
|
59
|
+
swap_owner,
|
|
60
|
+
)
|
|
45
61
|
from operate.utils.gnosis import transfer as transfer_from_safe
|
|
62
|
+
from operate.utils.gnosis import transfer_erc20_from_safe
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
logger = setup_logger(name="master_wallet")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# TODO Organize exceptions definition
|
|
69
|
+
class InsufficientFundsException(Exception):
|
|
70
|
+
"""Insufficient funds exception."""
|
|
46
71
|
|
|
47
72
|
|
|
48
73
|
class MasterWallet(LocalResource):
|
|
49
74
|
"""Master wallet."""
|
|
50
75
|
|
|
51
76
|
path: Path
|
|
52
|
-
|
|
77
|
+
address: str
|
|
78
|
+
|
|
79
|
+
safes: t.Dict[Chain, str] = field(default_factory=dict)
|
|
80
|
+
safe_chains: t.List[Chain] = field(default_factory=list)
|
|
53
81
|
ledger_type: LedgerType
|
|
82
|
+
safe_nonce: t.Optional[int] = None
|
|
54
83
|
|
|
55
84
|
_key: str
|
|
85
|
+
_mnemonic: str
|
|
56
86
|
_crypto: t.Optional[Crypto] = None
|
|
57
87
|
_password: t.Optional[str] = None
|
|
58
88
|
_crypto_cls: t.Type[Crypto]
|
|
@@ -81,70 +111,130 @@ class MasterWallet(LocalResource):
|
|
|
81
111
|
"""Key path."""
|
|
82
112
|
return self.path / self._key
|
|
83
113
|
|
|
114
|
+
@property
|
|
115
|
+
def mnemonic_path(self) -> Path:
|
|
116
|
+
"""Mnemonic path."""
|
|
117
|
+
return self.path / self._mnemonic
|
|
118
|
+
|
|
119
|
+
@staticmethod
|
|
84
120
|
def ledger_api(
|
|
85
|
-
|
|
86
|
-
chain_type: ChainType,
|
|
121
|
+
chain: Chain,
|
|
87
122
|
rpc: t.Optional[str] = None,
|
|
88
123
|
) -> LedgerApi:
|
|
89
124
|
"""Get ledger api object."""
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
chain_id=chain_type.id,
|
|
94
|
-
)
|
|
125
|
+
if not rpc:
|
|
126
|
+
return get_default_ledger_api(chain=chain)
|
|
127
|
+
return make_chain_ledger_api(chain=chain, rpc=rpc)
|
|
95
128
|
|
|
96
|
-
def transfer(
|
|
129
|
+
def transfer( # pylint: disable=too-many-arguments
|
|
97
130
|
self,
|
|
98
131
|
to: str,
|
|
99
132
|
amount: int,
|
|
100
|
-
|
|
133
|
+
chain: Chain,
|
|
134
|
+
asset: str = ZERO_ADDRESS,
|
|
101
135
|
from_safe: bool = True,
|
|
102
|
-
|
|
136
|
+
rpc: t.Optional[str] = None,
|
|
137
|
+
) -> t.Optional[str]:
|
|
103
138
|
"""Transfer funds to the given account."""
|
|
104
139
|
raise NotImplementedError()
|
|
105
140
|
|
|
106
|
-
|
|
107
|
-
def new(password: str, path: Path) -> t.Tuple["MasterWallet", t.List[str]]:
|
|
108
|
-
"""Create a new master wallet."""
|
|
109
|
-
raise NotImplementedError()
|
|
110
|
-
|
|
111
|
-
def create_safe(
|
|
141
|
+
def transfer_from_safe_then_eoa(
|
|
112
142
|
self,
|
|
113
|
-
|
|
114
|
-
|
|
143
|
+
to: str,
|
|
144
|
+
amount: int,
|
|
145
|
+
chain: Chain,
|
|
146
|
+
asset: str = ZERO_ADDRESS,
|
|
115
147
|
rpc: t.Optional[str] = None,
|
|
116
|
-
) ->
|
|
117
|
-
"""
|
|
148
|
+
) -> t.List[str]:
|
|
149
|
+
"""Transfer assets to the given account using Safe balance first, and EOA balance for leftover."""
|
|
118
150
|
raise NotImplementedError()
|
|
119
151
|
|
|
120
|
-
def
|
|
152
|
+
def drain(
|
|
121
153
|
self,
|
|
122
|
-
|
|
123
|
-
|
|
154
|
+
withdrawal_address: str,
|
|
155
|
+
chain: Chain,
|
|
156
|
+
from_safe: bool = True,
|
|
124
157
|
rpc: t.Optional[str] = None,
|
|
125
158
|
) -> None:
|
|
126
|
-
"""
|
|
159
|
+
"""Drain all erc20/native assets to the given account."""
|
|
160
|
+
raise NotImplementedError()
|
|
161
|
+
|
|
162
|
+
@classmethod
|
|
163
|
+
def new(cls, password: str, path: Path) -> t.Tuple["MasterWallet", t.List[str]]:
|
|
164
|
+
"""Create a new master wallet."""
|
|
165
|
+
raise NotImplementedError()
|
|
166
|
+
|
|
167
|
+
def decrypt_mnemonic(self, password: str) -> t.Optional[t.List[str]]:
|
|
168
|
+
"""Retrieve the mnemonic"""
|
|
127
169
|
raise NotImplementedError()
|
|
128
170
|
|
|
129
|
-
def
|
|
171
|
+
def create_safe(
|
|
130
172
|
self,
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
new_owner: str,
|
|
173
|
+
chain: Chain,
|
|
174
|
+
backup_owner: t.Optional[str] = None,
|
|
134
175
|
rpc: t.Optional[str] = None,
|
|
135
|
-
) ->
|
|
176
|
+
) -> t.Optional[str]:
|
|
136
177
|
"""Create safe."""
|
|
137
178
|
raise NotImplementedError()
|
|
138
179
|
|
|
139
|
-
def
|
|
180
|
+
def update_backup_owner(
|
|
140
181
|
self,
|
|
141
|
-
|
|
142
|
-
|
|
182
|
+
chain: Chain,
|
|
183
|
+
backup_owner: t.Optional[str] = None,
|
|
143
184
|
rpc: t.Optional[str] = None,
|
|
144
|
-
) ->
|
|
145
|
-
"""
|
|
185
|
+
) -> bool:
|
|
186
|
+
"""Update backup owner."""
|
|
146
187
|
raise NotImplementedError()
|
|
147
188
|
|
|
189
|
+
def is_password_valid(self, password: str) -> bool:
|
|
190
|
+
"""Verifies if the provided password is valid."""
|
|
191
|
+
try:
|
|
192
|
+
self._crypto_cls(self.path / self._key, password)
|
|
193
|
+
return True
|
|
194
|
+
except Exception: # pylint: disable=broad-except
|
|
195
|
+
return False
|
|
196
|
+
|
|
197
|
+
def update_password(self, new_password: str) -> None:
|
|
198
|
+
"""Update password."""
|
|
199
|
+
raise NotImplementedError()
|
|
200
|
+
|
|
201
|
+
def is_mnemonic_valid(self, mnemonic: str) -> bool:
|
|
202
|
+
"""Is mnemonic valid."""
|
|
203
|
+
raise NotImplementedError()
|
|
204
|
+
|
|
205
|
+
def update_password_with_mnemonic(self, mnemonic: str, new_password: str) -> None:
|
|
206
|
+
"""Updates password using the mnemonic."""
|
|
207
|
+
raise NotImplementedError()
|
|
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
|
+
|
|
227
|
+
# TODO move to resource.py if used in more resources similarly
|
|
228
|
+
@property
|
|
229
|
+
def extended_json(self) -> t.Dict:
|
|
230
|
+
"""Get JSON representation with extended information (e.g., safe owners)."""
|
|
231
|
+
raise NotImplementedError
|
|
232
|
+
|
|
233
|
+
@classmethod
|
|
234
|
+
def migrate_format(cls, path: Path) -> bool:
|
|
235
|
+
"""Migrate the JSON file format if needed."""
|
|
236
|
+
raise NotImplementedError
|
|
237
|
+
|
|
148
238
|
|
|
149
239
|
@dataclass
|
|
150
240
|
class EthereumMasterWallet(MasterWallet):
|
|
@@ -152,19 +242,70 @@ class EthereumMasterWallet(MasterWallet):
|
|
|
152
242
|
|
|
153
243
|
path: Path
|
|
154
244
|
address: str
|
|
155
|
-
safe_chains: t.List[ChainType] # For cross-chain support
|
|
156
245
|
|
|
246
|
+
safes: t.Dict[Chain, str] = field(default_factory=dict)
|
|
247
|
+
safe_chains: t.List[Chain] = field(default_factory=list)
|
|
157
248
|
ledger_type: LedgerType = LedgerType.ETHEREUM
|
|
158
|
-
safe: t.Optional[str] = None
|
|
159
249
|
safe_nonce: t.Optional[int] = None # For cross-chain reusability
|
|
160
250
|
|
|
161
251
|
_file = ledger_type.config_file
|
|
162
252
|
_key = ledger_type.key_file
|
|
253
|
+
_mnemonic = ledger_type.mnemonic_file
|
|
163
254
|
_crypto_cls = EthereumCrypto
|
|
164
255
|
|
|
165
|
-
def
|
|
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
|
+
|
|
285
|
+
def _transfer_from_eoa(
|
|
286
|
+
self, to: str, amount: int, chain: Chain, rpc: t.Optional[str] = None
|
|
287
|
+
) -> t.Optional[str]:
|
|
166
288
|
"""Transfer funds from EOA wallet."""
|
|
167
|
-
|
|
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
|
+
|
|
308
|
+
ledger_api = t.cast(EthereumApi, self.ledger_api(chain=chain, rpc=rpc))
|
|
168
309
|
tx_helper = TxSettler(
|
|
169
310
|
ledger_api=ledger_api,
|
|
170
311
|
crypto=self.crypto,
|
|
@@ -178,14 +319,20 @@ class EthereumMasterWallet(MasterWallet):
|
|
|
178
319
|
*args: t.Any, **kwargs: t.Any
|
|
179
320
|
) -> t.Dict:
|
|
180
321
|
"""Build transaction"""
|
|
322
|
+
max_priority_fee_per_gas = os.getenv("MAX_PRIORITY_FEE_PER_GAS", None)
|
|
323
|
+
max_fee_per_gas = os.getenv("MAX_FEE_PER_GAS", None)
|
|
181
324
|
tx = ledger_api.get_transfer_transaction(
|
|
182
325
|
sender_address=self.crypto.address,
|
|
183
326
|
destination_address=to,
|
|
184
327
|
amount=amount,
|
|
185
328
|
tx_fee=50000,
|
|
186
329
|
tx_nonce="0x",
|
|
187
|
-
chain_id=
|
|
330
|
+
chain_id=chain.id,
|
|
188
331
|
raise_on_try=True,
|
|
332
|
+
max_fee_per_gas=int(max_fee_per_gas) if max_fee_per_gas else None,
|
|
333
|
+
max_priority_fee_per_gas=(
|
|
334
|
+
int(max_priority_fee_per_gas) if max_priority_fee_per_gas else None
|
|
335
|
+
),
|
|
189
336
|
)
|
|
190
337
|
return ledger_api.update_with_gas_estimate(
|
|
191
338
|
transaction=tx,
|
|
@@ -193,48 +340,259 @@ class EthereumMasterWallet(MasterWallet):
|
|
|
193
340
|
)
|
|
194
341
|
|
|
195
342
|
setattr(tx_helper, "build", _build_tx) # noqa: B010
|
|
196
|
-
tx_helper.transact(lambda x: x, "", kwargs={})
|
|
343
|
+
tx_receipt = tx_helper.transact(lambda x: x, "", kwargs={})
|
|
344
|
+
tx_hash = tx_receipt.get("transactionHash", "").hex()
|
|
345
|
+
return tx_hash
|
|
197
346
|
|
|
198
|
-
def _transfer_from_safe(
|
|
347
|
+
def _transfer_from_safe(
|
|
348
|
+
self, to: str, amount: int, chain: Chain, rpc: t.Optional[str] = None
|
|
349
|
+
) -> t.Optional[str]:
|
|
199
350
|
"""Transfer funds from safe wallet."""
|
|
200
|
-
|
|
201
|
-
|
|
351
|
+
to = self._pre_transfer_checks(
|
|
352
|
+
to=to, amount=amount, chain=chain, from_safe=True
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
return transfer_from_safe(
|
|
356
|
+
ledger_api=self.ledger_api(chain=chain, rpc=rpc),
|
|
202
357
|
crypto=self.crypto,
|
|
203
|
-
safe=
|
|
358
|
+
safe=self.safes[chain],
|
|
204
359
|
to=to,
|
|
205
360
|
amount=amount,
|
|
206
361
|
)
|
|
207
362
|
|
|
208
|
-
def
|
|
363
|
+
def _transfer_erc20_from_safe(
|
|
209
364
|
self,
|
|
365
|
+
token: str,
|
|
210
366
|
to: str,
|
|
211
367
|
amount: int,
|
|
212
|
-
|
|
368
|
+
chain: Chain,
|
|
369
|
+
rpc: t.Optional[str] = None,
|
|
370
|
+
) -> t.Optional[str]:
|
|
371
|
+
"""Transfer erc20 from safe wallet."""
|
|
372
|
+
to = self._pre_transfer_checks(
|
|
373
|
+
to=to, amount=amount, chain=chain, from_safe=True, asset=token
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
return transfer_erc20_from_safe(
|
|
377
|
+
ledger_api=self.ledger_api(chain=chain, rpc=rpc),
|
|
378
|
+
crypto=self.crypto,
|
|
379
|
+
token=token,
|
|
380
|
+
safe=self.safes[chain],
|
|
381
|
+
to=to,
|
|
382
|
+
amount=amount,
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
def _transfer_erc20_from_eoa(
|
|
386
|
+
self,
|
|
387
|
+
token: str,
|
|
388
|
+
to: str,
|
|
389
|
+
amount: int,
|
|
390
|
+
chain: Chain,
|
|
391
|
+
rpc: t.Optional[str] = None,
|
|
392
|
+
) -> t.Optional[str]:
|
|
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
|
+
|
|
398
|
+
wallet_address = self.address
|
|
399
|
+
ledger_api = t.cast(EthereumApi, self.ledger_api(chain=chain, rpc=rpc))
|
|
400
|
+
tx_settler = TxSettler(
|
|
401
|
+
ledger_api=ledger_api,
|
|
402
|
+
crypto=self.crypto,
|
|
403
|
+
chain_type=ChainProfile.CUSTOM,
|
|
404
|
+
timeout=ON_CHAIN_INTERACT_TIMEOUT,
|
|
405
|
+
retries=ON_CHAIN_INTERACT_RETRIES,
|
|
406
|
+
sleep=ON_CHAIN_INTERACT_SLEEP,
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
def _build_transfer_tx( # pylint: disable=unused-argument
|
|
410
|
+
*args: t.Any, **kargs: t.Any
|
|
411
|
+
) -> t.Dict:
|
|
412
|
+
# TODO Backport to OpenAEA
|
|
413
|
+
instance = registry_contracts.erc20.get_instance(
|
|
414
|
+
ledger_api=ledger_api,
|
|
415
|
+
contract_address=token,
|
|
416
|
+
)
|
|
417
|
+
tx = instance.functions.transfer(to, amount).build_transaction(
|
|
418
|
+
{
|
|
419
|
+
"from": wallet_address,
|
|
420
|
+
"gas": 1,
|
|
421
|
+
"maxFeePerGas": 1,
|
|
422
|
+
"maxPriorityFeePerGas": 1,
|
|
423
|
+
"nonce": ledger_api.api.eth.get_transaction_count(wallet_address),
|
|
424
|
+
}
|
|
425
|
+
)
|
|
426
|
+
update_tx_with_gas_pricing(tx, ledger_api)
|
|
427
|
+
update_tx_with_gas_estimate(tx, ledger_api)
|
|
428
|
+
return tx
|
|
429
|
+
|
|
430
|
+
setattr(tx_settler, "build", _build_transfer_tx) # noqa: B010
|
|
431
|
+
tx_receipt = tx_settler.transact(
|
|
432
|
+
method=lambda: {},
|
|
433
|
+
contract="",
|
|
434
|
+
kwargs={},
|
|
435
|
+
dry_run=False,
|
|
436
|
+
)
|
|
437
|
+
tx_hash = tx_receipt.get("transactionHash", "").hex()
|
|
438
|
+
return tx_hash
|
|
439
|
+
|
|
440
|
+
def transfer( # pylint: disable=too-many-arguments
|
|
441
|
+
self,
|
|
442
|
+
to: str,
|
|
443
|
+
amount: int,
|
|
444
|
+
chain: Chain,
|
|
445
|
+
asset: str = ZERO_ADDRESS,
|
|
213
446
|
from_safe: bool = True,
|
|
214
|
-
|
|
447
|
+
rpc: t.Optional[str] = None,
|
|
448
|
+
) -> t.Optional[str]:
|
|
215
449
|
"""Transfer funds to the given account."""
|
|
216
450
|
if from_safe:
|
|
217
|
-
|
|
451
|
+
if asset == ZERO_ADDRESS:
|
|
452
|
+
return self._transfer_from_safe(
|
|
453
|
+
to=to,
|
|
454
|
+
amount=amount,
|
|
455
|
+
chain=chain,
|
|
456
|
+
rpc=rpc,
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
return self._transfer_erc20_from_safe(
|
|
460
|
+
token=asset,
|
|
218
461
|
to=to,
|
|
219
462
|
amount=amount,
|
|
220
|
-
|
|
463
|
+
chain=chain,
|
|
464
|
+
rpc=rpc,
|
|
221
465
|
)
|
|
222
|
-
|
|
466
|
+
|
|
467
|
+
if asset == ZERO_ADDRESS:
|
|
468
|
+
return self._transfer_from_eoa(
|
|
469
|
+
to=to,
|
|
470
|
+
amount=amount,
|
|
471
|
+
chain=chain,
|
|
472
|
+
rpc=rpc,
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
return self._transfer_erc20_from_eoa(
|
|
476
|
+
token=asset,
|
|
223
477
|
to=to,
|
|
224
478
|
amount=amount,
|
|
225
|
-
|
|
479
|
+
chain=chain,
|
|
480
|
+
rpc=rpc,
|
|
226
481
|
)
|
|
227
482
|
|
|
483
|
+
def transfer_from_safe_then_eoa(
|
|
484
|
+
self,
|
|
485
|
+
to: str,
|
|
486
|
+
amount: int,
|
|
487
|
+
chain: Chain,
|
|
488
|
+
asset: str = ZERO_ADDRESS,
|
|
489
|
+
rpc: t.Optional[str] = None,
|
|
490
|
+
) -> t.List[str]:
|
|
491
|
+
"""
|
|
492
|
+
Transfer assets to the given account using Safe balance first, and EOA balance for leftover.
|
|
493
|
+
|
|
494
|
+
If asset is a zero address, transfer native currency.
|
|
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
|
|
499
|
+
if asset == ZERO_ADDRESS:
|
|
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(
|
|
516
|
+
to=to,
|
|
517
|
+
amount=from_safe_amount,
|
|
518
|
+
chain=chain,
|
|
519
|
+
asset=asset,
|
|
520
|
+
from_safe=True,
|
|
521
|
+
rpc=rpc,
|
|
522
|
+
)
|
|
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
|
|
543
|
+
|
|
544
|
+
def drain(
|
|
545
|
+
self,
|
|
546
|
+
withdrawal_address: str,
|
|
547
|
+
chain: Chain,
|
|
548
|
+
from_safe: bool = True,
|
|
549
|
+
rpc: t.Optional[str] = None,
|
|
550
|
+
) -> None:
|
|
551
|
+
"""Drain all erc20/native assets to the given account."""
|
|
552
|
+
assets = [token[chain] for token in ERC20_TOKENS.values()] + [ZERO_ADDRESS]
|
|
553
|
+
for asset in assets:
|
|
554
|
+
balance = self.get_balance(chain=chain, asset=asset, from_safe=from_safe)
|
|
555
|
+
if balance <= 0:
|
|
556
|
+
continue
|
|
557
|
+
|
|
558
|
+
self.transfer(
|
|
559
|
+
to=withdrawal_address,
|
|
560
|
+
amount=balance,
|
|
561
|
+
chain=chain,
|
|
562
|
+
asset=asset,
|
|
563
|
+
from_safe=from_safe,
|
|
564
|
+
rpc=rpc,
|
|
565
|
+
)
|
|
566
|
+
|
|
228
567
|
@classmethod
|
|
229
568
|
def new(
|
|
230
569
|
cls, password: str, path: Path
|
|
231
570
|
) -> t.Tuple["EthereumMasterWallet", t.List[str]]:
|
|
232
571
|
"""Create a new master wallet."""
|
|
233
572
|
# Backport support on aea
|
|
573
|
+
|
|
574
|
+
eoa_wallet_path = path / cls._key
|
|
575
|
+
eoa_mnemonic_path = path / cls._mnemonic
|
|
576
|
+
|
|
577
|
+
if eoa_wallet_path.exists():
|
|
578
|
+
raise FileExistsError(f"Wallet file already exists at {eoa_wallet_path}.")
|
|
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
|
|
234
588
|
account = Account()
|
|
235
589
|
account.enable_unaudited_hdwallet_features()
|
|
236
590
|
crypto, mnemonic = account.create_with_mnemonic()
|
|
237
|
-
|
|
591
|
+
encrypted_mnemonic = EncryptedData.new(
|
|
592
|
+
path=eoa_mnemonic_path, password=password, plaintext_bytes=mnemonic.encode()
|
|
593
|
+
)
|
|
594
|
+
encrypted_mnemonic.store()
|
|
595
|
+
eoa_wallet_path.write_text(
|
|
238
596
|
data=json.dumps(
|
|
239
597
|
Account.encrypt(
|
|
240
598
|
private_key=crypto._private_key, # pylint: disable=protected-access
|
|
@@ -251,88 +609,281 @@ class EthereumMasterWallet(MasterWallet):
|
|
|
251
609
|
wallet.password = password
|
|
252
610
|
return wallet, mnemonic.split()
|
|
253
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
|
+
|
|
623
|
+
def update_password(self, new_password: str) -> None:
|
|
624
|
+
"""Updates password."""
|
|
625
|
+
create_backup(self.path / self._key)
|
|
626
|
+
self._crypto = None
|
|
627
|
+
(self.path / self._key).write_text(
|
|
628
|
+
data=json.dumps(
|
|
629
|
+
Account.encrypt(
|
|
630
|
+
private_key=self.crypto.private_key, # pylint: disable=protected-access
|
|
631
|
+
password=new_password,
|
|
632
|
+
),
|
|
633
|
+
indent=2,
|
|
634
|
+
),
|
|
635
|
+
encoding="utf-8",
|
|
636
|
+
)
|
|
637
|
+
self.password = new_password
|
|
638
|
+
|
|
639
|
+
def is_mnemonic_valid(self, mnemonic: str) -> bool:
|
|
640
|
+
"""Verifies if the provided BIP-39 mnemonic is valid."""
|
|
641
|
+
try:
|
|
642
|
+
w3 = Web3()
|
|
643
|
+
w3.eth.account.enable_unaudited_hdwallet_features()
|
|
644
|
+
new_account = w3.eth.account.from_mnemonic(mnemonic)
|
|
645
|
+
keystore_data = json.loads(
|
|
646
|
+
Path(self.path / self._key).read_text(encoding="utf-8")
|
|
647
|
+
)
|
|
648
|
+
stored_address = keystore_data["address"].removeprefix("0x").lower()
|
|
649
|
+
return stored_address == new_account.address.removeprefix("0x").lower()
|
|
650
|
+
except Exception: # pylint: disable=broad-except
|
|
651
|
+
return False
|
|
652
|
+
|
|
653
|
+
def update_password_with_mnemonic(self, mnemonic: str, new_password: str) -> None:
|
|
654
|
+
"""Updates password using the mnemonic."""
|
|
655
|
+
if not self.is_mnemonic_valid(mnemonic):
|
|
656
|
+
raise ValueError("The provided mnemonic is not valid")
|
|
657
|
+
|
|
658
|
+
path = self.path / EthereumMasterWallet._key
|
|
659
|
+
create_backup(path)
|
|
660
|
+
|
|
661
|
+
w3 = Web3()
|
|
662
|
+
w3.eth.account.enable_unaudited_hdwallet_features()
|
|
663
|
+
crypto = Web3().eth.account.from_mnemonic(mnemonic)
|
|
664
|
+
(path).write_text(
|
|
665
|
+
data=json.dumps(
|
|
666
|
+
Account.encrypt(
|
|
667
|
+
private_key=crypto._private_key, # pylint: disable=protected-access
|
|
668
|
+
password=new_password,
|
|
669
|
+
),
|
|
670
|
+
indent=2,
|
|
671
|
+
),
|
|
672
|
+
encoding="utf-8",
|
|
673
|
+
)
|
|
674
|
+
self.password = new_password
|
|
675
|
+
|
|
254
676
|
def create_safe(
|
|
255
677
|
self,
|
|
256
|
-
|
|
257
|
-
|
|
678
|
+
chain: Chain,
|
|
679
|
+
backup_owner: t.Optional[str] = None,
|
|
258
680
|
rpc: t.Optional[str] = None,
|
|
259
|
-
) ->
|
|
681
|
+
) -> t.Optional[str]:
|
|
260
682
|
"""Create safe."""
|
|
261
|
-
if
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
683
|
+
if chain in self.safes:
|
|
684
|
+
raise ValueError(f"Wallet already has a Safe on chain {chain}.")
|
|
685
|
+
|
|
686
|
+
safe, self.safe_nonce, tx_hash = create_gnosis_safe(
|
|
687
|
+
ledger_api=self.ledger_api(chain=chain, rpc=rpc),
|
|
265
688
|
crypto=self.crypto,
|
|
266
|
-
|
|
689
|
+
backup_owner=backup_owner,
|
|
267
690
|
salt_nonce=self.safe_nonce,
|
|
268
691
|
)
|
|
269
|
-
self.safe_chains.append(
|
|
692
|
+
self.safe_chains.append(chain)
|
|
693
|
+
if self.safes is None:
|
|
694
|
+
self.safes = {}
|
|
695
|
+
self.safes[chain] = safe
|
|
270
696
|
self.store()
|
|
697
|
+
return tx_hash
|
|
271
698
|
|
|
272
|
-
def
|
|
699
|
+
def update_backup_owner(
|
|
273
700
|
self,
|
|
274
|
-
|
|
275
|
-
|
|
701
|
+
chain: Chain,
|
|
702
|
+
backup_owner: t.Optional[str] = None,
|
|
276
703
|
rpc: t.Optional[str] = None,
|
|
277
|
-
) ->
|
|
278
|
-
"""
|
|
279
|
-
ledger_api = self.ledger_api(
|
|
280
|
-
if
|
|
281
|
-
raise ValueError("
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
704
|
+
) -> bool:
|
|
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."""
|
|
706
|
+
ledger_api = self.ledger_api(chain=chain, rpc=rpc)
|
|
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])
|
|
710
|
+
owners = get_owners(ledger_api=ledger_api, safe=safe)
|
|
711
|
+
|
|
712
|
+
if len(owners) > 2:
|
|
713
|
+
raise RuntimeError(
|
|
714
|
+
f"Safe {safe} on chain {chain} has more than 2 owners: {owners}."
|
|
715
|
+
)
|
|
288
716
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
chain_type: ChainType,
|
|
292
|
-
old_owner: str,
|
|
293
|
-
new_owner: str,
|
|
294
|
-
rpc: t.Optional[str] = None,
|
|
295
|
-
) -> None:
|
|
296
|
-
"""Swap backup owner."""
|
|
297
|
-
ledger_api = self.ledger_api(chain_type=chain_type, rpc=rpc)
|
|
298
|
-
if len(get_owners(ledger_api=ledger_api, safe=t.cast(str, self.safe))) == 1:
|
|
299
|
-
raise ValueError("Backup owner does not exist, cannot swap!")
|
|
300
|
-
swap_owner(
|
|
301
|
-
ledger_api=ledger_api,
|
|
302
|
-
safe=t.cast(str, self.safe),
|
|
303
|
-
old_owner=old_owner,
|
|
304
|
-
new_owner=new_owner,
|
|
305
|
-
crypto=self.crypto,
|
|
306
|
-
)
|
|
717
|
+
if backup_owner == safe:
|
|
718
|
+
raise ValueError("The Safe address cannot be set as the Safe backup owner.")
|
|
307
719
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
ledger_api = self.ledger_api(chain_type=chain_type, rpc=rpc)
|
|
316
|
-
owners = get_owners(ledger_api=ledger_api, safe=t.cast(str, self.safe))
|
|
317
|
-
if len(owners) == 1:
|
|
318
|
-
return self.add_backup_owner(chain_type=chain_type, owner=owner, rpc=rpc)
|
|
720
|
+
if backup_owner == self.address:
|
|
721
|
+
raise ValueError(
|
|
722
|
+
"The master wallet cannot be set as the Safe backup owner."
|
|
723
|
+
)
|
|
724
|
+
|
|
725
|
+
if self.address not in owners:
|
|
726
|
+
return False
|
|
319
727
|
|
|
320
728
|
owners.remove(self.address)
|
|
321
|
-
|
|
322
|
-
if old_owner == owner:
|
|
323
|
-
return None
|
|
729
|
+
old_backup_owner = owners[0] if owners else None
|
|
324
730
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
731
|
+
if old_backup_owner == backup_owner:
|
|
732
|
+
return False
|
|
733
|
+
|
|
734
|
+
if not old_backup_owner and backup_owner:
|
|
735
|
+
add_owner(
|
|
736
|
+
ledger_api=ledger_api,
|
|
737
|
+
safe=safe,
|
|
738
|
+
owner=backup_owner,
|
|
739
|
+
crypto=self.crypto,
|
|
740
|
+
)
|
|
741
|
+
return True
|
|
742
|
+
if old_backup_owner and not backup_owner:
|
|
743
|
+
remove_owner(
|
|
744
|
+
ledger_api=ledger_api,
|
|
745
|
+
safe=safe,
|
|
746
|
+
owner=old_backup_owner,
|
|
747
|
+
crypto=self.crypto,
|
|
748
|
+
threshold=1,
|
|
749
|
+
)
|
|
750
|
+
return True
|
|
751
|
+
if old_backup_owner and backup_owner:
|
|
752
|
+
swap_owner(
|
|
753
|
+
ledger_api=ledger_api,
|
|
754
|
+
safe=safe,
|
|
755
|
+
old_owner=old_backup_owner,
|
|
756
|
+
new_owner=backup_owner,
|
|
757
|
+
crypto=self.crypto,
|
|
758
|
+
)
|
|
759
|
+
return True
|
|
760
|
+
|
|
761
|
+
return False
|
|
762
|
+
|
|
763
|
+
@property
|
|
764
|
+
def extended_json(self) -> t.Dict:
|
|
765
|
+
"""Get JSON representation with extended information (e.g., safe owners)."""
|
|
766
|
+
rpc = None
|
|
767
|
+
wallet_json = self.json
|
|
768
|
+
|
|
769
|
+
balances: t.Dict[str, t.Dict[str, t.Dict[str, int]]] = {}
|
|
770
|
+
owner_sets = set()
|
|
771
|
+
for chain, safe in self.safes.items():
|
|
772
|
+
chain_str = chain.value
|
|
773
|
+
ledger_api = self.ledger_api(chain=chain, rpc=rpc)
|
|
774
|
+
owners = get_owners(ledger_api=ledger_api, safe=safe)
|
|
775
|
+
|
|
776
|
+
if self.address in owners:
|
|
777
|
+
owners.remove(self.address)
|
|
778
|
+
|
|
779
|
+
balances[chain_str] = {self.address: {}, safe: {}}
|
|
780
|
+
|
|
781
|
+
assets = [token[chain] for token in ERC20_TOKENS.values()] + [ZERO_ADDRESS]
|
|
782
|
+
for asset in assets:
|
|
783
|
+
balances[chain_str][self.address][asset] = self.get_balance(
|
|
784
|
+
chain=chain, asset=asset, from_safe=False
|
|
785
|
+
)
|
|
786
|
+
balances[chain_str][safe][asset] = self.get_balance(
|
|
787
|
+
chain=chain, asset=asset, from_safe=True
|
|
788
|
+
)
|
|
789
|
+
wallet_json["safes"][chain_str] = {
|
|
790
|
+
safe: {
|
|
791
|
+
"backup_owners": owners,
|
|
792
|
+
"balances": balances[chain_str][safe],
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
owner_sets.add(frozenset(owners))
|
|
796
|
+
|
|
797
|
+
wallet_json["balances"] = balances
|
|
798
|
+
wallet_json["extended_json"] = True
|
|
799
|
+
wallet_json["all_safes_have_backup_owner"] = all(
|
|
800
|
+
len(owners) > 0 for owners in owner_sets
|
|
801
|
+
)
|
|
802
|
+
wallet_json["consistent_safe_address"] = len(set(self.safes.values())) == 1
|
|
803
|
+
wallet_json["consistent_backup_owner"] = len(owner_sets) == 1
|
|
804
|
+
wallet_json["consistent_backup_owner_count"] = all(
|
|
805
|
+
len(owner) == 1 for owner in owner_sets
|
|
330
806
|
)
|
|
807
|
+
return wallet_json
|
|
331
808
|
|
|
332
809
|
@classmethod
|
|
333
810
|
def load(cls, path: Path) -> "EthereumMasterWallet":
|
|
334
811
|
"""Load master wallet."""
|
|
335
|
-
|
|
812
|
+
# TODO: This is a complex way to read the 'safes' dictionary.
|
|
813
|
+
# The reason for that is that wallet.safes[chain] would fail
|
|
814
|
+
# (for example in service manager) when passed a ChainType key.
|
|
815
|
+
|
|
816
|
+
raw_ethereum_wallet = t.cast(EthereumMasterWallet, super().load(path)) # type: ignore
|
|
817
|
+
safes = {}
|
|
818
|
+
for chain, safe_address in raw_ethereum_wallet.safes.items():
|
|
819
|
+
safes[Chain(chain)] = safe_address
|
|
820
|
+
|
|
821
|
+
raw_ethereum_wallet.safes = safes
|
|
822
|
+
return raw_ethereum_wallet
|
|
823
|
+
|
|
824
|
+
@classmethod
|
|
825
|
+
def migrate_format(cls, path: Path) -> bool:
|
|
826
|
+
"""Migrate the JSON file format if needed."""
|
|
827
|
+
wallet_path = path / cls._file
|
|
828
|
+
with open(wallet_path, "r", encoding="utf-8") as file:
|
|
829
|
+
data = json.load(file)
|
|
830
|
+
|
|
831
|
+
migrated = False
|
|
832
|
+
if "safes" not in data:
|
|
833
|
+
safes = {}
|
|
834
|
+
for chain in data["safe_chains"]:
|
|
835
|
+
safes[chain] = data["safe"]
|
|
836
|
+
data.pop("safe")
|
|
837
|
+
data["safes"] = safes
|
|
838
|
+
migrated = True
|
|
839
|
+
|
|
840
|
+
old_to_new_chains = [
|
|
841
|
+
"ethereum",
|
|
842
|
+
"goerli",
|
|
843
|
+
"gnosis",
|
|
844
|
+
"solana",
|
|
845
|
+
"optimism",
|
|
846
|
+
"base",
|
|
847
|
+
"mode",
|
|
848
|
+
]
|
|
849
|
+
safe_chains = []
|
|
850
|
+
for chain in data["safe_chains"]:
|
|
851
|
+
if isinstance(chain, int):
|
|
852
|
+
safe_chains.append(old_to_new_chains[chain])
|
|
853
|
+
migrated = True
|
|
854
|
+
else:
|
|
855
|
+
safe_chains.append(chain)
|
|
856
|
+
data["safe_chains"] = safe_chains
|
|
857
|
+
|
|
858
|
+
if isinstance(data["ledger_type"], int):
|
|
859
|
+
old_to_new_ledgers = [ledger_type.value for ledger_type in LedgerType]
|
|
860
|
+
data["ledger_type"] = old_to_new_ledgers[data["ledger_type"]]
|
|
861
|
+
migrated = True
|
|
862
|
+
|
|
863
|
+
safes = {}
|
|
864
|
+
for chain, address in data["safes"].items():
|
|
865
|
+
if str(chain).isnumeric():
|
|
866
|
+
safes[old_to_new_chains[int(chain)]] = address
|
|
867
|
+
migrated = True
|
|
868
|
+
else:
|
|
869
|
+
safes[chain] = address
|
|
870
|
+
data["safes"] = safes
|
|
871
|
+
|
|
872
|
+
if "optimistic" in data.get("safes", {}):
|
|
873
|
+
data["safes"]["optimism"] = data["safes"].pop("optimistic")
|
|
874
|
+
migrated = True
|
|
875
|
+
|
|
876
|
+
if "optimistic" in data.get("safe_chains"):
|
|
877
|
+
data["safe_chains"] = [
|
|
878
|
+
"optimism" if chain == "optimistic" else chain
|
|
879
|
+
for chain in data["safe_chains"]
|
|
880
|
+
]
|
|
881
|
+
migrated = True
|
|
882
|
+
|
|
883
|
+
with open(wallet_path, "w", encoding="utf-8") as file:
|
|
884
|
+
json.dump(data, file, indent=2)
|
|
885
|
+
|
|
886
|
+
return migrated
|
|
336
887
|
|
|
337
888
|
|
|
338
889
|
LEDGER_TYPE_TO_WALLET_CLASS = {
|
|
@@ -343,7 +894,11 @@ LEDGER_TYPE_TO_WALLET_CLASS = {
|
|
|
343
894
|
class MasterWalletManager:
|
|
344
895
|
"""Master wallet manager."""
|
|
345
896
|
|
|
346
|
-
def __init__(
|
|
897
|
+
def __init__(
|
|
898
|
+
self,
|
|
899
|
+
path: Path,
|
|
900
|
+
password: t.Optional[str] = None,
|
|
901
|
+
) -> None:
|
|
347
902
|
"""Initialize master wallet manager."""
|
|
348
903
|
self.path = path
|
|
349
904
|
self._password = password
|
|
@@ -354,14 +909,14 @@ class MasterWalletManager:
|
|
|
354
909
|
return [wallet.json for wallet in self]
|
|
355
910
|
|
|
356
911
|
@property
|
|
357
|
-
def password(self) -> str:
|
|
912
|
+
def password(self) -> t.Optional[str]:
|
|
358
913
|
"""Password string."""
|
|
359
914
|
if self._password is None:
|
|
360
915
|
raise ValueError("Password not set.")
|
|
361
916
|
return self._password
|
|
362
917
|
|
|
363
918
|
@password.setter
|
|
364
|
-
def password(self, value: str) -> None:
|
|
919
|
+
def password(self, value: t.Optional[str]) -> None:
|
|
365
920
|
"""Set password value."""
|
|
366
921
|
self._password = value
|
|
367
922
|
|
|
@@ -406,6 +961,36 @@ class MasterWalletManager:
|
|
|
406
961
|
wallet.password = self.password
|
|
407
962
|
return wallet
|
|
408
963
|
|
|
964
|
+
def is_password_valid(self, password: str) -> bool:
|
|
965
|
+
"""Verifies if the provided password is valid."""
|
|
966
|
+
for wallet in self:
|
|
967
|
+
if not wallet.is_password_valid(password):
|
|
968
|
+
return False
|
|
969
|
+
|
|
970
|
+
return True
|
|
971
|
+
|
|
972
|
+
def update_password(self, new_password: str) -> None:
|
|
973
|
+
"""Updates password of manager and wallets."""
|
|
974
|
+
for wallet in self:
|
|
975
|
+
wallet.password = self.password
|
|
976
|
+
wallet.update_password(new_password)
|
|
977
|
+
|
|
978
|
+
self.password = new_password
|
|
979
|
+
|
|
980
|
+
def is_mnemonic_valid(self, mnemonic: str) -> bool:
|
|
981
|
+
"""Verifies if the provided BIP-39 mnemonic is valid."""
|
|
982
|
+
for wallet in self:
|
|
983
|
+
if not wallet.is_mnemonic_valid(mnemonic):
|
|
984
|
+
return False
|
|
985
|
+
return True
|
|
986
|
+
|
|
987
|
+
def update_password_with_mnemonic(self, mnemonic: str, new_password: str) -> None:
|
|
988
|
+
"""Updates password using the mnemonic."""
|
|
989
|
+
for wallet in self:
|
|
990
|
+
wallet.update_password_with_mnemonic(mnemonic, new_password)
|
|
991
|
+
|
|
992
|
+
self.password = new_password
|
|
993
|
+
|
|
409
994
|
def __iter__(self) -> t.Iterator[MasterWallet]:
|
|
410
995
|
"""Iterate over master wallets."""
|
|
411
996
|
for ledger_type in LedgerType:
|