• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

safe-global / safe-cli / 8157330034

05 Mar 2024 01:47PM UTC coverage: 95.161%. Remained the same
8157330034

push

github

Uxio0
Rename `master` -> `main`

2163 of 2273 relevant lines covered (95.16%)

3.76 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.93
/safe_cli/operators/safe_operator.py
1
import dataclasses
4✔
2
import json
4✔
3
import os
4✔
4
from functools import cached_property, wraps
4✔
5
from typing import List, Optional, Sequence, Set, Tuple
4✔
6

7
from ens import ENS
4✔
8
from eth_account import Account
4✔
9
from eth_account.signers.local import LocalAccount
4✔
10
from eth_typing import ChecksumAddress
4✔
11
from eth_utils import ValidationError
4✔
12
from hexbytes import HexBytes
4✔
13
from packaging import version as semantic_version
4✔
14
from prompt_toolkit import HTML, print_formatted_text
4✔
15
from web3 import Web3
4✔
16
from web3.contract import Contract
4✔
17
from web3.exceptions import BadFunctionCallOutput
4✔
18

19
from gnosis.eth import (
4✔
20
    EthereumClient,
21
    EthereumNetwork,
22
    EthereumNetworkNotSupported,
23
    TxSpeed,
24
)
25
from gnosis.eth.clients import EtherscanClient, EtherscanClientConfigurationProblem
4✔
26
from gnosis.eth.constants import NULL_ADDRESS, SENTINEL_ADDRESS
4✔
27
from gnosis.eth.contracts import (
4✔
28
    get_erc20_contract,
29
    get_erc721_contract,
30
    get_safe_V1_1_1_contract,
31
    get_sign_message_lib_contract,
32
)
33
from gnosis.eth.eip712 import eip712_encode
4✔
34
from gnosis.eth.utils import get_empty_tx_params
4✔
35
from gnosis.safe import InvalidInternalTx, Safe, SafeOperationEnum, SafeTx
4✔
36
from gnosis.safe.api import TransactionServiceApi
4✔
37
from gnosis.safe.multi_send import MultiSend, MultiSendOperation, MultiSendTx
4✔
38
from gnosis.safe.safe_deployments import safe_deployments
4✔
39

40
from safe_cli.ethereum_hd_wallet import get_account_from_words
4✔
41
from safe_cli.operators.exceptions import (
4✔
42
    AccountNotLoadedException,
43
    ExistingOwnerException,
44
    FallbackHandlerNotSupportedException,
45
    GuardNotSupportedException,
46
    HashAlreadyApproved,
47
    InvalidFallbackHandlerException,
48
    InvalidGuardException,
49
    InvalidMasterCopyException,
50
    InvalidMigrationContractException,
51
    InvalidNonceException,
52
    NonExistingOwnerException,
53
    NotEnoughEtherToSend,
54
    NotEnoughSignatures,
55
    SafeAlreadyUpdatedException,
56
    SafeVersionNotSupportedException,
57
    SameFallbackHandlerException,
58
    SameGuardException,
59
    SameMasterCopyException,
60
    SenderRequiredException,
61
    ThresholdLimitException,
62
    UpdateAddressesNotValid,
63
)
64
from safe_cli.safe_addresses import (
4✔
65
    get_default_fallback_handler_address,
66
    get_last_sign_message_lib_address,
67
    get_safe_contract_address,
68
    get_safe_l2_contract_address,
69
)
70
from safe_cli.utils import choose_option_from_list, get_erc_20_list, yes_or_no_question
4✔
71

72
from ..contracts import safe_to_l2_migration
4✔
73
from .hw_wallets.hw_wallet import HwWallet
4✔
74
from .hw_wallets.hw_wallet_manager import HwWalletType, get_hw_wallet_manager
4✔
75

76

77
@dataclasses.dataclass
4✔
78
class SafeCliInfo:
4✔
79
    address: str
4✔
80
    nonce: int
4✔
81
    threshold: int
4✔
82
    owners: List[str]
4✔
83
    master_copy: str
4✔
84
    modules: List[str]
4✔
85
    fallback_handler: str
4✔
86
    guard: str
4✔
87
    balance_ether: int
4✔
88
    version: str
4✔
89

90
    def __str__(self):
4✔
91
        return (
4✔
92
            f"safe-version={self.version} nonce={self.nonce} threshold={self.threshold} owners={self.owners} "
4✔
93
            f"master-copy={self.master_copy} fallback-hander={self.fallback_handler} "
3✔
94
            f"modules={self.modules} balance-ether={self.balance_ether:.4f}"
3✔
95
        )
96

97

98
def require_tx_service(f):
4✔
99
    @wraps(f)
4✔
100
    def decorated(self, *args, **kwargs):
4✔
101
        if not self.safe_tx_service:
×
102
            print_formatted_text(
103
                HTML(
104
                    f"<ansired>No tx service available for "
105
                    f"network={self.network.name}</ansired>"
106
                )
107
            )
108
            if self.etherscan:
×
109
                url = f"{self.etherscan.base_url}/address/{self.address}"
×
110
                print_formatted_text(HTML(f"<b>Try Etherscan instead</b> {url}"))
×
111
        else:
×
112
            return f(self, *args, **kwargs)
×
113

114
    return decorated
4✔
115

116

117
def require_default_sender(f):
4✔
118
    """
119
    Throws SenderRequiredException if not default sender configured
120
    """
121

122
    @wraps(f)
4✔
123
    def decorated(self, *args, **kwargs):
4✔
124
        if not self.default_sender and not self.hw_wallet_manager.sender:
4✔
125
            raise SenderRequiredException()
4✔
126
        else:
×
127
            return f(self, *args, **kwargs)
4✔
128

129
    return decorated
4✔
130

131

132
class SafeOperator:
4✔
133
    address: ChecksumAddress
4✔
134
    node_url: str
4✔
135
    ethereum_client: EthereumClient
4✔
136
    ens: ENS
4✔
137
    network: EthereumNetwork
4✔
138
    etherscan: Optional[EtherscanClient]
4✔
139
    safe_tx_service: Optional[TransactionServiceApi]
4✔
140
    safe: Safe
4✔
141
    safe_contract: Contract
4✔
142
    safe_contract_1_1_0: Contract
4✔
143
    accounts: Set[LocalAccount] = set()
4✔
144
    default_sender: Optional[LocalAccount]
4✔
145
    executed_transactions: List[str]
4✔
146
    _safe_cli_info: Optional[SafeCliInfo]
4✔
147
    require_all_signatures: bool
4✔
148

149
    def __init__(self, address: ChecksumAddress, node_url: str):
4✔
150
        self.address = address
4✔
151
        self.node_url = node_url
4✔
152
        self.ethereum_client = EthereumClient(self.node_url)
4✔
153
        self.ens = ENS.from_web3(self.ethereum_client.w3)
4✔
154
        self.network: EthereumNetwork = self.ethereum_client.get_network()
4✔
155
        try:
4✔
156
            self.etherscan = EtherscanClient(self.network)
4✔
157
        except EtherscanClientConfigurationProblem:
4✔
158
            self.etherscan = None
4✔
159

160
        try:
4✔
161
            self.safe_tx_service = TransactionServiceApi.from_ethereum_client(
4✔
162
                self.ethereum_client
4✔
163
            )
164
        except EthereumNetworkNotSupported:
4✔
165
            self.safe_tx_service = None
4✔
166

167
        self.safe = Safe(address, self.ethereum_client)
4✔
168
        self.safe_contract = self.safe.contract
4✔
169
        self.safe_contract_1_1_0 = get_safe_V1_1_1_contract(
4✔
170
            self.ethereum_client.w3, address=self.address
4✔
171
        )
172
        self.accounts: Set[LocalAccount] = set()
4✔
173
        self.default_sender: Optional[LocalAccount] = None
4✔
174
        self.executed_transactions: List[str] = []
4✔
175
        self._safe_cli_info: Optional[SafeCliInfo] = None  # Cache for SafeCliInfo
4✔
176
        self.require_all_signatures = (
4✔
177
            True  # Require all signatures to be present to send a tx
4✔
178
        )
179
        self.hw_wallet_manager = get_hw_wallet_manager()
4✔
180

181
    @cached_property
4✔
182
    def last_default_fallback_handler_address(self) -> ChecksumAddress:
4✔
183
        """
184
        :return: Address for last version of default fallback handler contract
185
        """
186
        return get_default_fallback_handler_address(self.ethereum_client)
×
187

188
    @cached_property
4✔
189
    def last_safe_contract_address(self) -> ChecksumAddress:
4✔
190
        """
191
        :return: Last version of the Safe Contract. Use events version for every network but mainnet
192
        """
193
        if self.network == EthereumNetwork.MAINNET:
×
194
            return get_safe_contract_address(self.ethereum_client)
×
195
        else:
×
196
            return get_safe_l2_contract_address(self.ethereum_client)
×
197

198
    @cached_property
4✔
199
    def ens_domain(self) -> Optional[str]:
4✔
200
        # FIXME After web3.py fixes the middleware copy
201
        if self.network == EthereumNetwork.MAINNET:
4✔
202
            return self.ens.name(self.address)
×
203

204
    @property
4✔
205
    def safe_cli_info(self) -> SafeCliInfo:
4✔
206
        if not self._safe_cli_info:
4✔
207
            self._safe_cli_info = self.refresh_safe_cli_info()
4✔
208
        return self._safe_cli_info
4✔
209

210
    def refresh_safe_cli_info(self) -> SafeCliInfo:
4✔
211
        self._safe_cli_info = self.get_safe_cli_info()
4✔
212
        return self._safe_cli_info
4✔
213

214
    def is_version_updated(self) -> bool:
4✔
215
        """
216
        :return: True if Safe Master Copy is updated, False otherwise
217
        """
218

219
        last_safe_contract_address = self.last_safe_contract_address
4✔
220
        if self.safe_cli_info.master_copy == last_safe_contract_address:
4✔
221
            return True
×
222
        else:  # Check versions, maybe safe-cli addresses were not updated
×
223
            try:
4✔
224
                safe_contract_version = Safe(
4✔
225
                    last_safe_contract_address, self.ethereum_client
4✔
226
                ).retrieve_version()
2✔
227
            except BadFunctionCallOutput:  # Safe master copy is not deployed or errored, maybe custom network
×
228
                return True  # We cannot say you are not updated ¯\_(ツ)_/¯
×
229

230
            return semantic_version.parse(
4✔
231
                self.safe_cli_info.version
4✔
232
            ) >= semantic_version.parse(safe_contract_version)
4✔
233

234
    def load_cli_owners_from_words(self, words: List[str]):
4✔
235
        if len(words) == 1:  # Reading seed from Environment Variable
×
236
            words = os.environ.get(words[0], default="").strip().split(" ")
×
237
        parsed_words = " ".join(words)
×
238
        try:
×
239
            for index in range(100):  # Try first accounts of seed phrase
×
240
                account = get_account_from_words(parsed_words, index=index)
×
241
                if account.address in self.safe_cli_info.owners:
×
242
                    self.load_cli_owners([account.key.hex()])
×
243
            if not index:
×
244
                print_formatted_text(
245
                    HTML(
246
                        "<ansired>Cannot generate any valid owner for this Safe</ansired>"
247
                    )
248
                )
249
        except ValidationError:
×
250
            print_formatted_text(
251
                HTML("<ansired>Cannot load owners from words</ansired>")
252
            )
253

254
    def load_cli_owners(self, keys: List[str]):
4✔
255
        for key in keys:
4✔
256
            try:
4✔
257
                account = Account.from_key(
4✔
258
                    os.environ.get(key, default=key)
4✔
259
                )  # Try to get key from `environ`
260
                self.accounts.add(account)
4✔
261
                balance = self.ethereum_client.get_balance(account.address)
4✔
262
                print_formatted_text(
4✔
263
                    HTML(
4✔
264
                        f"Loaded account <b>{account.address}</b> "
4✔
265
                        f'with balance={Web3.from_wei(balance, "ether")} ether'
3✔
266
                    )
267
                )
268
                if (
4✔
269
                    not self.default_sender
4✔
270
                    and not self.hw_wallet_manager.sender
4✔
271
                    and balance > 0
4✔
272
                ):
273
                    print_formatted_text(
4✔
274
                        HTML(
4✔
275
                            f"Set account <b>{account.address}</b> as default sender of txs"
4✔
276
                        )
277
                    )
278
                    self.default_sender = account
4✔
279
            except ValueError:
4✔
280
                print_formatted_text(HTML(f"<ansired>Cannot load key={key}</ansired>"))
4✔
281

282
    def load_hw_wallet(
4✔
283
        self,
284
        hw_wallet_type: HwWalletType,
4✔
285
        derivation_path: str,
4✔
286
        template_derivation_path: str,
4✔
287
    ):
288
        if not self.hw_wallet_manager.is_supported_hw_wallet(hw_wallet_type):
4✔
289
            return None
290
        if derivation_path is None:
4✔
291
            ledger_accounts = self.hw_wallet_manager.get_accounts(
4✔
292
                hw_wallet_type, template_derivation_path
4✔
293
            )
294
            if len(ledger_accounts) == 0:
4✔
295
                return None
4✔
296

297
            option = choose_option_from_list(
4✔
298
                "Select the owner address", ledger_accounts
4✔
299
            )
300
            if option is None:
4✔
301
                return None
302
            _, derivation_path = ledger_accounts[option]
4✔
303
        address = self.hw_wallet_manager.add_account(hw_wallet_type, derivation_path)
4✔
304
        balance = self.ethereum_client.get_balance(address)
4✔
305

306
        print_formatted_text(
4✔
307
            HTML(
4✔
308
                f"Loaded account <b>{address}</b> "
4✔
309
                f'with balance={Web3.from_wei(balance, "ether")} ether.'
3✔
310
            )
311
        )
312

313
        if (
4✔
314
            not self.default_sender
4✔
315
            and not self.hw_wallet_manager.sender
4✔
316
            and balance > 0
4✔
317
        ):
318
            self.hw_wallet_manager.set_sender(hw_wallet_type, derivation_path)
4✔
319
            print_formatted_text(HTML(f"HwDevice {address} added as sender"))
4✔
320
        else:
321
            print_formatted_text(HTML(f"HwDevice {address} wasn't added as sender"))
4✔
322

323
    def load_ledger_cli_owners(
4✔
324
        self, derivation_path: str = None, legacy_account: bool = False
4✔
325
    ):
326
        if legacy_account:
4✔
327
            self.load_hw_wallet(HwWalletType.LEDGER, derivation_path, "44'/60'/0'/{i}")
328
        else:
329
            self.load_hw_wallet(
4✔
330
                HwWalletType.LEDGER, derivation_path, "44'/60'/{i}'/0/0"
4✔
331
            )
332

333
    def load_trezor_cli_owners(
4✔
334
        self, derivation_path: str = None, legacy_account: bool = False
4✔
335
    ):
336
        if legacy_account:
337
            self.load_hw_wallet(HwWalletType.TREZOR, derivation_path, "44'/60'/0'/{i}")
338
        else:
339
            self.load_hw_wallet(
340
                HwWalletType.TREZOR, derivation_path, "44'/60'/0'/0/{i}"
341
            )
342

343
    def unload_cli_owners(self, owners: List[str]):
4✔
344
        accounts_to_remove: Set[Account] = set()
4✔
345
        for owner in owners:
4✔
346
            for account in self.accounts:
4✔
347
                if account.address == owner:
4✔
348
                    if self.default_sender and self.default_sender.address == owner:
4✔
349
                        self.default_sender = None
4✔
350
                    accounts_to_remove.add(account)
4✔
351
                    break
4✔
352
        self.accounts = self.accounts.difference(accounts_to_remove)
4✔
353
        # Check if there are ledger owners
354
        if self.hw_wallet_manager.wallets and len(accounts_to_remove) < len(owners):
4✔
355
            accounts_to_remove = (
4✔
356
                accounts_to_remove | self.hw_wallet_manager.delete_accounts(owners)
4✔
357
            )
358

359
        if accounts_to_remove:
4✔
360
            print_formatted_text(
4✔
361
                HTML("<ansigreen>Accounts have been deleted</ansigreen>")
4✔
362
            )
363
        else:
364
            print_formatted_text(HTML("<ansired>No account was deleted</ansired>"))
365

366
    def show_cli_owners(self):
4✔
367
        accounts = self.accounts | self.hw_wallet_manager.wallets
368
        if not accounts:
369
            print_formatted_text(HTML("<ansired>No accounts loaded</ansired>"))
370
        else:
371
            for account in accounts:
372
                print_formatted_text(
373
                    HTML(
374
                        f"<ansigreen><b>Account</b> {account.address} loaded</ansigreen>"
375
                    )
376
                )
377
            if self.default_sender:
378
                print_formatted_text(
379
                    HTML(
380
                        f"<ansigreen><b>Default sender:</b> {self.default_sender.address}"
381
                        f"</ansigreen>"
382
                    )
383
                )
384
            elif self.hw_wallet_manager.sender:
385
                print_formatted_text(
386
                    HTML(
387
                        f"<ansigreen><b>HwDevice sender:</b> {self.hw_wallet_manager.sender}"
388
                        f"</ansigreen>"
389
                    )
390
                )
391
            else:
392
                print_formatted_text(
393
                    HTML("<ansigreen>Not default sender set </ansigreen>")
394
                )
395

396
    def approve_hash(self, hash_to_approve: HexBytes, sender: str) -> bool:
4✔
397
        sender_account = [
4✔
398
            account for account in self.accounts if account.address == sender
4✔
399
        ]
400
        if not sender_account:
4✔
401
            raise AccountNotLoadedException(sender)
4✔
402
        elif sender not in self.safe_cli_info.owners:
4✔
403
            raise NonExistingOwnerException(sender)
4✔
404
        elif self.safe.retrieve_is_hash_approved(
4✔
405
            self.default_sender.address, hash_to_approve
4✔
406
        ):
407
            raise HashAlreadyApproved(hash_to_approve, self.default_sender.address)
4✔
408
        else:
409
            sender_account = sender_account[0]
4✔
410
            transaction_to_send = self.safe_contract.functions.approveHash(
4✔
411
                hash_to_approve
4✔
412
            ).build_transaction(
2✔
413
                {
4✔
414
                    "from": sender_account.address,
4✔
415
                    "nonce": self.ethereum_client.get_nonce_for_account(
4✔
416
                        sender_account.address
4✔
417
                    ),
418
                }
419
            )
420
            if self.ethereum_client.is_eip1559_supported():
4✔
421
                transaction_to_send = self.ethereum_client.set_eip1559_fees(
4✔
422
                    transaction_to_send
4✔
423
                )
424
            call_result = self.ethereum_client.w3.eth.call(transaction_to_send)
4✔
425
            if call_result:  # There's revert message
4✔
426
                return False
427
            else:
428
                signed_transaction = sender_account.sign_transaction(
4✔
429
                    transaction_to_send
4✔
430
                )
431
                tx_hash = self.ethereum_client.send_raw_transaction(
4✔
432
                    signed_transaction["rawTransaction"]
4✔
433
                )
434
                print_formatted_text(
4✔
435
                    HTML(
4✔
436
                        f"<ansigreen>Sent tx with tx-hash {tx_hash.hex()} from owner "
4✔
437
                        f"{self.default_sender.address}, waiting for receipt</ansigreen>"
3✔
438
                    )
439
                )
440
                if self.ethereum_client.get_transaction_receipt(tx_hash, timeout=120):
4✔
441
                    return True
4✔
442
                else:
443
                    print_formatted_text(
444
                        HTML(
445
                            f"<ansired>Tx with tx-hash {tx_hash.hex()} still not mined</ansired>"
446
                        )
447
                    )
448
                    return False
449

450
    def sign_message(
4✔
451
        self,
452
        eip191_message: Optional[str] = None,
4✔
453
        eip712_message_path: Optional[str] = None,
4✔
454
    ) -> bool:
4✔
455
        if eip712_message_path:
4✔
456
            try:
4✔
457
                message = json.load(open(eip712_message_path, "r"))
4✔
458
                message_bytes = b"".join(eip712_encode(message))
4✔
459
            except ValueError:
460
                raise ValueError
461
        else:
462
            message = eip191_message
4✔
463
            message_bytes = eip191_message.encode("UTF-8")
4✔
464

465
        safe_message_hash = self.safe.get_message_hash(message_bytes)
4✔
466

467
        sign_message_lib_address = get_last_sign_message_lib_address(
4✔
468
            self.ethereum_client
4✔
469
        )
470
        contract = get_sign_message_lib_contract(self.ethereum_client.w3, self.address)
4✔
471
        sign_message_data = HexBytes(
4✔
472
            contract.functions.signMessage(message_bytes).build_transaction(
4✔
473
                get_empty_tx_params(),
4✔
474
            )["data"]
4✔
475
        )
476
        print_formatted_text(HTML(f"Signing message: \n {message}"))
4✔
477
        if self.prepare_and_execute_safe_transaction(
4✔
478
            sign_message_lib_address,
4✔
479
            0,
4✔
480
            sign_message_data,
4✔
481
            operation=SafeOperationEnum.DELEGATE_CALL,
4✔
482
        ):
483
            print_formatted_text(
4✔
484
                HTML(f"Message was signed correctly: {safe_message_hash.hex()}")
4✔
485
            )
486

487
    def add_owner(self, new_owner: str, threshold: Optional[int] = None) -> bool:
4✔
488
        threshold = threshold if threshold is not None else self.safe_cli_info.threshold
4✔
489
        if new_owner in self.safe_cli_info.owners:
4✔
490
            raise ExistingOwnerException(new_owner)
4✔
491
        else:
492
            # TODO Allow to set threshold
493
            transaction = self.safe_contract.functions.addOwnerWithThreshold(
4✔
494
                new_owner, threshold
4✔
495
            ).build_transaction({"from": self.address, "gas": 0, "gasPrice": 0})
4✔
496
            if self.execute_safe_internal_transaction(transaction["data"]):
4✔
497
                self.safe_cli_info.owners = self.safe.retrieve_owners()
4✔
498
                self.safe_cli_info.threshold = threshold
4✔
499
                return True
4✔
500
            return False
501

502
    def remove_owner(self, owner_to_remove: str, threshold: Optional[int] = None):
4✔
503
        threshold = threshold if threshold is not None else self.safe_cli_info.threshold
4✔
504
        if owner_to_remove not in self.safe_cli_info.owners:
4✔
505
            raise NonExistingOwnerException(owner_to_remove)
4✔
506
        elif len(self.safe_cli_info.owners) == threshold:
4✔
507
            raise ThresholdLimitException()
508
        else:
509
            index_owner = self.safe_cli_info.owners.index(owner_to_remove)
4✔
510
            prev_owner = (
4✔
511
                self.safe_cli_info.owners[index_owner - 1]
4✔
512
                if index_owner
4✔
513
                else SENTINEL_ADDRESS
4✔
514
            )
515
            transaction = self.safe_contract.functions.removeOwner(
4✔
516
                prev_owner, owner_to_remove, threshold
4✔
517
            ).build_transaction({"from": self.address, "gas": 0, "gasPrice": 0})
4✔
518
            if self.execute_safe_internal_transaction(transaction["data"]):
4✔
519
                self.safe_cli_info.owners = self.safe.retrieve_owners()
4✔
520
                self.safe_cli_info.threshold = threshold
4✔
521
                return True
4✔
522
            return False
523

524
    def send_custom(
4✔
525
        self,
526
        to: str,
4✔
527
        value: int,
4✔
528
        data: bytes,
4✔
529
        safe_nonce: Optional[int] = None,
4✔
530
        delegate_call: bool = False,
4✔
531
    ) -> bool:
4✔
532
        if value > 0:
4✔
533
            safe_balance = self.ethereum_client.get_balance(self.address)
4✔
534
            if safe_balance < value:
4✔
535
                raise NotEnoughEtherToSend(safe_balance)
4✔
536
        operation = (
4✔
537
            SafeOperationEnum.DELEGATE_CALL if delegate_call else SafeOperationEnum.CALL
4✔
538
        )
539
        return self.prepare_and_execute_safe_transaction(
4✔
540
            to, value, data, operation, safe_nonce=safe_nonce
4✔
541
        )
542

543
    def send_ether(self, to: str, value: int, **kwargs) -> bool:
4✔
544
        return self.send_custom(to, value, b"", **kwargs)
4✔
545

546
    def send_erc20(self, to: str, token_address: str, amount: int, **kwargs) -> bool:
4✔
547
        transaction = (
548
            get_erc20_contract(self.ethereum_client.w3, token_address)
549
            .functions.transfer(to, amount)
550
            .build_transaction({"from": self.address, "gas": 0, "gasPrice": 0})
551
        )
552
        return self.send_custom(
553
            token_address, 0, HexBytes(transaction["data"]), **kwargs
554
        )
555

556
    def send_erc721(self, to: str, token_address: str, token_id: int, **kwargs) -> bool:
4✔
557
        transaction = (
558
            get_erc721_contract(self.ethereum_client.w3, token_address)
559
            .functions.transferFrom(self.address, to, token_id)
560
            .build_transaction({"from": self.address, "gas": 0, "gasPrice": 0})
561
        )
562
        return self.send_custom(token_address, 0, transaction["data"], **kwargs)
563

564
    def change_fallback_handler(self, new_fallback_handler: str) -> bool:
4✔
565
        if new_fallback_handler == self.safe_cli_info.fallback_handler:
4✔
566
            raise SameFallbackHandlerException(new_fallback_handler)
4✔
567
        elif semantic_version.parse(
4✔
568
            self.safe_cli_info.version
4✔
569
        ) < semantic_version.parse("1.1.0"):
4✔
570
            raise FallbackHandlerNotSupportedException()
4✔
571
        elif (
2✔
572
            new_fallback_handler != NULL_ADDRESS
4✔
573
            and not self.ethereum_client.is_contract(new_fallback_handler)
4✔
574
        ):
575
            raise InvalidFallbackHandlerException(
4✔
576
                f"{new_fallback_handler} address is not a contract"
4✔
577
            )
578
        else:
579
            transaction = self.safe_contract.functions.setFallbackHandler(
4✔
580
                new_fallback_handler
4✔
581
            ).build_transaction({"from": self.address, "gas": 0, "gasPrice": 0})
4✔
582
            if self.execute_safe_internal_transaction(transaction["data"]):
4✔
583
                self.safe_cli_info.fallback_handler = new_fallback_handler
4✔
584
                self.safe_cli_info.version = self.safe.retrieve_version()
4✔
585
                return True
4✔
586

587
    def change_guard(self, guard: str) -> bool:
4✔
588
        if guard == self.safe_cli_info.guard:
4✔
589
            raise SameGuardException(guard)
4✔
590
        elif semantic_version.parse(
4✔
591
            self.safe_cli_info.version
4✔
592
        ) < semantic_version.parse("1.3.0"):
4✔
593
            raise GuardNotSupportedException()
4✔
594
        elif guard != NULL_ADDRESS and not self.ethereum_client.is_contract(guard):
4✔
595
            raise InvalidGuardException(f"{guard} address is not a contract")
4✔
596
        else:
597
            transaction = self.safe_contract.functions.setGuard(
4✔
598
                guard
4✔
599
            ).build_transaction({"from": self.address, "gas": 0, "gasPrice": 0})
4✔
600
            if self.execute_safe_internal_transaction(transaction["data"]):
4✔
601
                self.safe_cli_info.guard = guard
4✔
602
                self.safe_cli_info.version = self.safe.retrieve_version()
4✔
603
                return True
4✔
604

605
    def change_master_copy(self, new_master_copy: str) -> bool:
4✔
606
        if new_master_copy == self.safe_cli_info.master_copy:
4✔
607
            raise SameMasterCopyException(new_master_copy)
4✔
608
        else:
609
            safe_version = self.safe.retrieve_version()
4✔
610
            if semantic_version.parse(safe_version) >= semantic_version.parse("1.3.0"):
4✔
611
                raise SafeVersionNotSupportedException(
4✔
612
                    f"{safe_version} cannot be updated (yet)"
4✔
613
                )
614

615
            try:
4✔
616
                Safe(new_master_copy, self.ethereum_client).retrieve_version()
4✔
617
            except BadFunctionCallOutput:
4✔
618
                raise InvalidMasterCopyException(new_master_copy)
4✔
619

620
            transaction = self.safe_contract_1_1_0.functions.changeMasterCopy(
4✔
621
                new_master_copy
4✔
622
            ).build_transaction({"from": self.address, "gas": 0, "gasPrice": 0})
4✔
623
            if self.execute_safe_internal_transaction(transaction["data"]):
4✔
624
                self.safe_cli_info.master_copy = new_master_copy
4✔
625
                self.safe_cli_info.version = self.safe.retrieve_version()
4✔
626
                return True
4✔
627

628
    def update_version(self) -> Optional[bool]:
4✔
629
        """
630
        Update Safe Master Copy and Fallback handler to the last version
631

632
        :return:
633
        """
634

635
        safe_version = self.safe.retrieve_version()
4✔
636
        if semantic_version.parse(safe_version) >= semantic_version.parse("1.3.0"):
4✔
637
            raise SafeVersionNotSupportedException(
4✔
638
                f"{safe_version} cannot be updated (yet)"
4✔
639
            )
640

641
        if self.is_version_updated():
4✔
642
            raise SafeAlreadyUpdatedException(f"{safe_version} already updated")
643

644
        addresses = (
4✔
645
            self.last_safe_contract_address,
4✔
646
            self.last_default_fallback_handler_address,
4✔
647
        )
648
        if not all(
4✔
649
            self.ethereum_client.is_contract(contract) for contract in addresses
4✔
650
        ):
651
            raise UpdateAddressesNotValid(
652
                "Not valid addresses to update Safe", *addresses
653
            )
654

655
        multisend = MultiSend(ethereum_client=self.ethereum_client)
4✔
656
        tx_params = {"from": self.address, "gas": 0, "gasPrice": 0}
4✔
657
        multisend_txs = [
4✔
658
            MultiSendTx(MultiSendOperation.CALL, self.address, 0, data)
4✔
659
            for data in (
4✔
660
                self.safe_contract_1_1_0.functions.changeMasterCopy(
4✔
661
                    self.last_safe_contract_address
4✔
662
                ).build_transaction(tx_params)["data"],
4✔
663
                self.safe_contract_1_1_0.functions.setFallbackHandler(
4✔
664
                    self.last_default_fallback_handler_address
4✔
665
                ).build_transaction(tx_params)["data"],
4✔
666
            )
667
        ]
668

669
        multisend_data = multisend.build_tx_data(multisend_txs)
4✔
670

671
        if self.prepare_and_execute_safe_transaction(
4✔
672
            multisend.address,
4✔
673
            0,
4✔
674
            multisend_data,
4✔
675
            operation=SafeOperationEnum.DELEGATE_CALL,
4✔
676
        ):
677
            self.safe_cli_info.master_copy = self.last_safe_contract_address
4✔
678
            self.safe_cli_info.fallback_handler = (
4✔
679
                self.last_default_fallback_handler_address
4✔
680
            )
681
            self.safe_cli_info.version = self.safe.retrieve_version()
4✔
682

683
    def update_version_to_l2(
4✔
684
        self, migration_contract_address: ChecksumAddress
4✔
685
    ) -> Optional[bool]:
4✔
686
        """
687
        Update not L2 Safe to L2, so official UI supports it. Useful when replaying Safes deployed in
688
        non L2 networks (like mainnet) in L2 networks.
689
        Only v1.1.1, v1.3.0 and v1.4.1 versions are supported. Also, Safe nonce must be 0.
690

691
        :return:
692
        """
693

694
        if not self.ethereum_client.is_contract(migration_contract_address):
4✔
695
            raise InvalidMigrationContractException(
696
                f"Non L2 to L2 migration contract {migration_contract_address} is not deployed"
697
            )
698

699
        safe_version = self.safe.retrieve_version()
4✔
700
        chain_id = self.ethereum_client.get_chain_id()
4✔
701

702
        if self.safe.retrieve_nonce() > 0:
4✔
703
            raise InvalidNonceException("Nonce must be 0 for non L2 to L2 migration")
704

705
        l2_migration_contract = self.ethereum_client.w3.eth.contract(
4✔
706
            NULL_ADDRESS, abi=safe_to_l2_migration["abi"]
4✔
707
        )
708
        if safe_version == "1.1.1":
4✔
709
            safe_l2_singleton = safe_deployments["1.3.0"]["GnosisSafeL2"][str(chain_id)]
4✔
710
            fallback_handler = safe_deployments["1.3.0"][
4✔
711
                "CompatibilityFallbackHandler"
4✔
712
            ][str(chain_id)]
4✔
713
            data = HexBytes(
4✔
714
                l2_migration_contract.functions.migrateFromV111(
4✔
715
                    safe_l2_singleton, fallback_handler
4✔
716
                ).build_transaction(get_empty_tx_params())["data"]
4✔
717
            )
718
        elif safe_version in ("1.3.0", "1.4.1"):
4✔
719
            safe_l2_singleton = safe_deployments[safe_version]["GnosisSafeL2"][
4✔
720
                str(chain_id)
4✔
721
            ]
722
            fallback_handler = self.safe_cli_info.fallback_handler
4✔
723
            data = HexBytes(
4✔
724
                l2_migration_contract.functions.migrateToL2(
4✔
725
                    safe_l2_singleton
4✔
726
                ).build_transaction(get_empty_tx_params())["data"]
4✔
727
            )
728
        else:
729
            raise InvalidMasterCopyException(
730
                "Current version is not supported to migrate to L2"
731
            )
732

733
        if self.prepare_and_execute_safe_transaction(
4✔
734
            migration_contract_address,
4✔
735
            0,
4✔
736
            data,
4✔
737
            operation=SafeOperationEnum.DELEGATE_CALL,
4✔
738
        ):
739
            self.safe_cli_info.master_copy = safe_l2_singleton
4✔
740
            self.safe_cli_info.fallback_handler = fallback_handler
4✔
741
            self.safe_cli_info.version = self.safe.retrieve_version()
4✔
742

743
    def change_threshold(self, threshold: int):
4✔
744
        if threshold == self.safe_cli_info.threshold:
4✔
745
            print_formatted_text(
746
                HTML(f"<ansired>Threshold is already {threshold}</ansired>")
747
            )
748
        elif threshold > len(self.safe_cli_info.owners):
4✔
749
            print_formatted_text(
750
                HTML(
751
                    f"<ansired>Threshold={threshold} bigger than number "
752
                    f"of owners={len(self.safe_cli_info.owners)}</ansired>"
753
                )
754
            )
755
        else:
756
            transaction = self.safe_contract.functions.changeThreshold(
4✔
757
                threshold
4✔
758
            ).build_transaction({"from": self.address, "gas": 0, "gasPrice": 0})
4✔
759

760
            if self.execute_safe_internal_transaction(transaction["data"]):
4✔
761
                self.safe_cli_info.threshold = threshold
4✔
762

763
    def enable_module(self, module_address: str):
4✔
764
        if module_address in self.safe_cli_info.modules:
765
            print_formatted_text(
766
                HTML(f"<ansired>Module {module_address} is already enabled</ansired>")
767
            )
768
        else:
769
            transaction = self.safe_contract.functions.enableModule(
770
                module_address
771
            ).build_transaction({"from": self.address, "gas": 0, "gasPrice": 0})
772
            if self.execute_safe_internal_transaction(transaction["data"]):
773
                self.safe_cli_info.modules = self.safe.retrieve_modules()
774

775
    def disable_module(self, module_address: str):
4✔
776
        if module_address not in self.safe_cli_info.modules:
777
            print_formatted_text(
778
                HTML(f"<ansired>Module {module_address} is not enabled</ansired>")
779
            )
780
        else:
781
            pos = self.safe_cli_info.modules.index(module_address)
782
            if pos == 0:
783
                previous_address = SENTINEL_ADDRESS
784
            else:
785
                previous_address = self.safe_cli_info.modules[pos - 1]
786
            transaction = self.safe_contract.functions.disableModule(
787
                previous_address, module_address
788
            ).build_transaction({"from": self.address, "gas": 0, "gasPrice": 0})
789
            if self.execute_safe_internal_transaction(transaction["data"]):
790
                self.safe_cli_info.modules = self.safe.retrieve_modules()
791

792
    def print_info(self):
4✔
793
        for key, value in dataclasses.asdict(self.safe_cli_info).items():
4✔
794
            print_formatted_text(
4✔
795
                HTML(
4✔
796
                    f"<b><ansigreen>{key.capitalize()}</ansigreen></b>="
4✔
797
                    f"<ansiblue>{value}</ansiblue>"
3✔
798
                )
799
            )
800
        if self.ens_domain:
4✔
801
            print_formatted_text(
802
                HTML(
803
                    f"<b><ansigreen>Ens domain</ansigreen></b>="
804
                    f"<ansiblue>{self.ens_domain}</ansiblue>"
805
                )
806
            )
807
        if self.safe_tx_service:
4✔
808
            url = f"{self.safe_tx_service.base_url}/api/v1/safes/{self.address}/transactions/"
4✔
809
            print_formatted_text(
4✔
810
                HTML(
4✔
811
                    f"<b><ansigreen>Safe Tx Service</ansigreen></b>="
4✔
812
                    f"<ansiblue>{url}</ansiblue>"
3✔
813
                )
814
            )
815

816
        if self.etherscan:
4✔
817
            url = f"{self.etherscan.base_url}/address/{self.address}"
4✔
818
            print_formatted_text(
4✔
819
                HTML(
4✔
820
                    f"<b><ansigreen>Etherscan</ansigreen></b>="
4✔
821
                    f"<ansiblue>{url}</ansiblue>"
3✔
822
                )
823
            )
824

825
        if not self.hw_wallet_manager.is_supported_hw_wallet(HwWalletType.LEDGER):
4✔
826
            print_formatted_text(
827
                HTML(
828
                    "<b><ansigreen>Ledger</ansigreen></b>="
829
                    "<ansired>Disabled </ansired> <b>Optional ledger library is not installed, run pip install safe-cli[ledger] </b>"
830
                )
831
            )
832
        else:
833
            print_formatted_text(
4✔
834
                HTML(
4✔
835
                    "<b><ansigreen>Ledger</ansigreen></b>="
4✔
836
                    "<ansiblue>supported</ansiblue>"
837
                )
838
            )
839

840
        if not self.hw_wallet_manager.is_supported_hw_wallet(HwWalletType.TREZOR):
4✔
841
            print_formatted_text(
842
                HTML(
843
                    "<b><ansigreen>Trezor</ansigreen></b>="
844
                    "<ansired>Disabled </ansired> <b>Optional trezor library is not installed, run pip install safe-cli[trezor] </b>"
845
                )
846
            )
847
        else:
848
            print_formatted_text(
4✔
849
                HTML(
4✔
850
                    "<b><ansigreen>Trezor</ansigreen></b>="
4✔
851
                    "<ansiblue>supported</ansiblue>"
852
                )
853
            )
854

855
        if not self.is_version_updated():
4✔
856
            print_formatted_text(
857
                HTML(
858
                    "<ansired>Safe is not updated! You can use <b>update</b> command to update "
859
                    "the Safe to a newest version</ansired>"
860
                )
861
            )
862

863
    def get_safe_cli_info(self) -> SafeCliInfo:
4✔
864
        safe = self.safe
4✔
865
        balance_ether = Web3.from_wei(
4✔
866
            self.ethereum_client.get_balance(self.address), "ether"
4✔
867
        )
868
        safe_info = safe.retrieve_all_info()
4✔
869
        return SafeCliInfo(
4✔
870
            self.address,
4✔
871
            safe_info.nonce,
4✔
872
            safe_info.threshold,
4✔
873
            safe_info.owners,
4✔
874
            safe_info.master_copy,
4✔
875
            safe_info.modules,
4✔
876
            safe_info.fallback_handler,
4✔
877
            safe_info.guard,
4✔
878
            balance_ether,
4✔
879
            safe_info.version,
4✔
880
        )
881

882
    def get_threshold(self):
4✔
883
        print_formatted_text(self.safe.retrieve_threshold())
884

885
    def get_nonce(self):
4✔
886
        print_formatted_text(self.safe.retrieve_nonce())
4✔
887

888
    def get_owners(self):
4✔
889
        print_formatted_text(self.safe.retrieve_owners())
890

891
    def execute_safe_internal_transaction(self, data: bytes) -> bool:
4✔
892
        return self.prepare_and_execute_safe_transaction(self.address, 0, data)
4✔
893

894
    def prepare_safe_transaction(
4✔
895
        self,
896
        to: str,
4✔
897
        value: int,
4✔
898
        data: bytes,
4✔
899
        operation: SafeOperationEnum = SafeOperationEnum.CALL,
4✔
900
        safe_nonce: Optional[int] = None,
4✔
901
    ) -> SafeTx:
4✔
902
        safe_tx = self.safe.build_multisig_tx(
4✔
903
            to, value, data, operation=operation.value, safe_nonce=safe_nonce
4✔
904
        )
905
        self.sign_transaction(safe_tx)  # Raises exception if it cannot be signed
4✔
906
        return safe_tx
4✔
907

908
    def prepare_and_execute_safe_transaction(
4✔
909
        self,
910
        to: str,
4✔
911
        value: int,
4✔
912
        data: bytes,
4✔
913
        operation: SafeOperationEnum = SafeOperationEnum.CALL,
4✔
914
        safe_nonce: Optional[int] = None,
4✔
915
    ) -> bool:
4✔
916
        safe_tx = self.prepare_safe_transaction(
4✔
917
            to, value, data, operation, safe_nonce=safe_nonce
4✔
918
        )
919
        return self.execute_safe_transaction(safe_tx)
4✔
920

921
    @require_default_sender  # Throws Exception if default sender not found
4✔
922
    def execute_safe_transaction(self, safe_tx: SafeTx):
4✔
923
        try:
4✔
924
            if self.default_sender:
4✔
925
                call_result = safe_tx.call(self.default_sender.address)
4✔
926
            else:
927
                call_result = safe_tx.call(self.hw_wallet_manager.sender.address)
928
            print_formatted_text(HTML(f"Result: <ansigreen>{call_result}</ansigreen>"))
4✔
929
            if yes_or_no_question("Do you want to execute tx " + str(safe_tx)):
4✔
930
                if self.default_sender:
4✔
931
                    tx_hash, tx = safe_tx.execute(
4✔
932
                        self.default_sender.key, eip1559_speed=TxSpeed.NORMAL
4✔
933
                    )
934
                else:
935
                    tx_hash, tx = self.hw_wallet_manager.execute_safe_tx(
936
                        safe_tx, eip1559_speed=TxSpeed.NORMAL
937
                    )
938
                self.executed_transactions.append(tx_hash.hex())
4✔
939
                print_formatted_text(
4✔
940
                    HTML(
4✔
941
                        f"<ansigreen>Sent tx with tx-hash {tx_hash.hex()} "
4✔
942
                        f"and safe-nonce {safe_tx.safe_nonce}, waiting for receipt</ansigreen>"
3✔
943
                    )
944
                )
945
                tx_receipt = self.ethereum_client.get_transaction_receipt(
4✔
946
                    tx_hash, timeout=120
4✔
947
                )
948
                if tx_receipt:
4✔
949
                    fees = self.ethereum_client.w3.from_wei(
4✔
950
                        tx_receipt["gasUsed"]
4✔
951
                        * tx_receipt.get("effectiveGasPrice", tx.get("gasPrice", 0)),
4✔
952
                        "ether",
4✔
953
                    )
954
                    print_formatted_text(
4✔
955
                        HTML(
4✔
956
                            f"<ansigreen>Tx was executed on block-number={tx_receipt['blockNumber']}, fees "
4✔
957
                            f"deducted={fees}</ansigreen>"
3✔
958
                        )
959
                    )
960
                    self.safe_cli_info.nonce += 1
4✔
961
                    return True
4✔
962
                else:
963
                    print_formatted_text(
964
                        HTML(
965
                            f"<ansired>Tx with tx-hash {tx_hash.hex()} still not mined</ansired>"
966
                        )
967
                    )
968
        except InvalidInternalTx as invalid_internal_tx:
969
            print_formatted_text(
970
                HTML(f"Result: <ansired>InvalidTx - {invalid_internal_tx}</ansired>")
971
            )
972
        return False
973

974
    # Batch_transactions multisend
975
    def batch_safe_txs(
4✔
976
        self, safe_nonce: int, safe_txs: Sequence[SafeTx]
4✔
977
    ) -> Optional[SafeTx]:
4✔
978
        """
979
        Submit signatures to the tx service. It's recommended to be on Safe v1.3.0 to prevent issues
980
        with `safeTxGas` and gas estimation.
981

982
        :return:
983
        """
984

985
        try:
4✔
986
            multisend = MultiSend(ethereum_client=self.ethereum_client)
4✔
987
        except ValueError:
988
            multisend = None
989
            print_formatted_text(
990
                HTML(
991
                    "<ansired>Multisend contract is not deployed on this network and it's required for "
992
                    "batching txs</ansired>"
993
                )
994
            )
995

996
        multisend_txs = []
4✔
997
        for safe_tx in safe_txs:
4✔
998
            # Check if call is already a Multisend call
999
            inner_txs = MultiSend.from_transaction_data(safe_tx.data)
4✔
1000
            if inner_txs:
4✔
1001
                multisend_txs.extend(inner_txs)
1002
            else:
1003
                multisend_txs.append(
4✔
1004
                    MultiSendTx(
4✔
1005
                        MultiSendOperation.CALL, safe_tx.to, safe_tx.value, safe_tx.data
4✔
1006
                    )
1007
                )
1008

1009
        if len(multisend_txs) == 1:
4✔
1010
            safe_tx.safe_tx_gas = 0
1011
            safe_tx.base_gas = 0
1012
            safe_tx.gas_price = 0
1013
            safe_tx.signatures = b""
1014
            safe_tx.safe_nonce = safe_nonce  # Resend single transaction
1015
        elif multisend:
4✔
1016
            safe_tx = SafeTx(
4✔
1017
                self.ethereum_client,
4✔
1018
                self.address,
4✔
1019
                multisend.address,
4✔
1020
                0,
4✔
1021
                multisend.build_tx_data(multisend_txs),
4✔
1022
                SafeOperationEnum.DELEGATE_CALL.value,
4✔
1023
                0,
4✔
1024
                0,
4✔
1025
                0,
4✔
1026
                None,
4✔
1027
                None,
4✔
1028
                safe_nonce=safe_nonce,
4✔
1029
            )
1030
        else:
1031
            # Multisend not defined
1032
            return None
1033

1034
        safe_tx = self.sign_transaction(safe_tx)
4✔
1035
        if not safe_tx.signatures:
4✔
1036
            print_formatted_text(
1037
                HTML("<ansired>At least one owner must be loaded</ansired>")
1038
            )
1039
            return None
1040
        else:
1041
            return safe_tx
4✔
1042

1043
    def get_signers(self) -> Tuple[List[LocalAccount], List[HwWallet]]:
4✔
1044
        """
1045

1046
        :return: Tuple with eoa signers and hw_wallet signers
1047
        """
1048
        permitted_signers = self.get_permitted_signers()
4✔
1049
        threshold = self.safe_cli_info.threshold
4✔
1050
        eoa_signers: List[
4✔
1051
            Account
1052
        ] = []  # Some accounts that are not an owner can be loaded
4✔
1053
        for account in self.accounts:
4✔
1054
            if account.address in permitted_signers:
4✔
1055
                eoa_signers.append(account)
4✔
1056
                threshold -= 1
4✔
1057
                if threshold == 0:
4✔
1058
                    break
4✔
1059
        # If still pending required signatures continue with ledger owners
1060
        hw_wallet_signers = []
4✔
1061
        if threshold > 0 and self.hw_wallet_manager.wallets:
4✔
1062
            for hw_wallet in self.hw_wallet_manager.wallets:
1063
                if hw_wallet.address in permitted_signers:
1064
                    hw_wallet_signers.append(hw_wallet)
1065
                    threshold -= 1
1066
                    if threshold == 0:
1067
                        break
1068

1069
        if self.require_all_signatures and threshold > 0:
4✔
1070
            raise NotEnoughSignatures(threshold)
4✔
1071

1072
        return (eoa_signers, hw_wallet_signers)
4✔
1073

1074
    # TODO Set sender so we can save gas in that signature
1075
    def sign_transaction(self, safe_tx: SafeTx) -> SafeTx:
4✔
1076
        eoa_signers, hw_wallets_signers = self.get_signers()
4✔
1077
        for selected_account in eoa_signers:
4✔
1078
            safe_tx.sign(selected_account.key)
4✔
1079

1080
        # Sign with ledger
1081
        if len(hw_wallets_signers):
4✔
1082
            safe_tx = self.hw_wallet_manager.sign_safe_tx(safe_tx, hw_wallets_signers)
1083

1084
        return safe_tx
4✔
1085

1086
    @require_tx_service
4✔
1087
    def _require_tx_service_mode(self):
4✔
1088
        print_formatted_text(
1089
            HTML(
1090
                "<ansired>First enter tx-service mode using <b>tx-service</b> command</ansired>"
1091
            )
1092
        )
1093

1094
    def get_delegates(self):
4✔
1095
        return self._require_tx_service_mode()
1096

1097
    def add_delegate(self, delegate_address: str, label: str, signer_address: str):
4✔
1098
        return self._require_tx_service_mode()
1099

1100
    def remove_delegate(self, delegate_address: str, signer_address: str):
4✔
1101
        return self._require_tx_service_mode()
1102

1103
    def submit_signatures(self, safe_tx_hash: bytes) -> bool:
4✔
1104
        return self._require_tx_service_mode()
1105

1106
    def get_balances(self):
4✔
1107
        return self._require_tx_service_mode()
1108

1109
    def get_transaction_history(self):
4✔
1110
        return self._require_tx_service_mode()
1111

1112
    def batch_txs(self, safe_nonce: int, safe_tx_hashes: Sequence[bytes]) -> bool:
4✔
1113
        return self._require_tx_service_mode()
1114

1115
    def execute_tx(self, safe_tx_hash: Sequence[bytes]) -> bool:
4✔
1116
        return self._require_tx_service_mode()
1117

1118
    def get_permitted_signers(self) -> Set[ChecksumAddress]:
4✔
1119
        """
1120
        :return: Accounts that can sign a transaction
1121
        """
1122
        return set(self.safe_cli_info.owners)
4✔
1123

1124
    def drain(self, to: str):
4✔
1125
        # Getting all events related with ERC20 transfers
1126
        last = self.ethereum_client.get_block("latest")["number"]
4✔
1127
        token_addresses = get_erc_20_list(self.ethereum_client, self.address, 1, last)
4✔
1128
        safe_txs = []
4✔
1129
        for token_address in token_addresses:
4✔
1130
            balance = self.ethereum_client.erc20.get_balance(
4✔
1131
                self.address, token_address
4✔
1132
            )
1133
            if balance > 0:
4✔
1134
                transaction = (
4✔
1135
                    get_erc20_contract(self.ethereum_client.w3, token_address)
4✔
1136
                    .functions.transfer(to, balance)
4✔
1137
                    .build_transaction({"from": self.address, "gas": 0, "gasPrice": 0})
4✔
1138
                )
1139

1140
                safe_tx = self.prepare_safe_transaction(
4✔
1141
                    token_address,
4✔
1142
                    0,
4✔
1143
                    HexBytes(transaction["data"]),
4✔
1144
                    SafeOperationEnum.CALL,
4✔
1145
                    safe_nonce=None,
4✔
1146
                )
1147
                safe_txs.append(safe_tx)
4✔
1148

1149
        # Getting ethereum balance
1150
        balance_eth = self.ethereum_client.get_balance(self.address)
4✔
1151
        if balance_eth:
4✔
1152
            safe_tx = self.prepare_safe_transaction(
4✔
1153
                to,
4✔
1154
                balance_eth,
4✔
1155
                b"",
4✔
1156
                SafeOperationEnum.CALL,
4✔
1157
                safe_nonce=None,
4✔
1158
            )
1159
            safe_txs.append(safe_tx)
4✔
1160

1161
        if safe_txs:
4✔
1162
            multisend_tx = self.batch_safe_txs(self.get_nonce(), safe_txs)
4✔
1163
            if multisend_tx is not None:
4✔
1164
                if self.execute_safe_transaction(multisend_tx):
4✔
1165
                    print_formatted_text(
4✔
1166
                        HTML(
4✔
1167
                            "<ansigreen>Transaction to drain account correctly executed</ansigreen>"
4✔
1168
                        )
1169
                    )
1170
        else:
1171
            print_formatted_text(
1172
                HTML("<ansigreen>Safe account is currently empty</ansigreen>")
1173
            )
1174

1175
    def remove_proposed_transaction(self, safe_tx_hash: bytes):
4✔
1176
        return self._require_tx_service_mode()
1177

1178
    def process_command(self, first_command: str, rest_command: List[str]) -> bool:
4✔
1179
        if first_command == "help":
1180
            print_formatted_text("I still cannot help you")
1181
        elif first_command == "refresh":
1182
            print_formatted_text("Reloading Safe information")
1183
            self.refresh_safe_cli_info()
1184

1185
        return False
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc