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

safe-global / safe-cli / 9698628578

27 Jun 2024 02:38PM CUT coverage: 88.571% (+0.07%) from 88.505%
9698628578

push

github

web-flow
Add hw wallet sign message (#420)

* Add support for sign_message HwWallet

* Add Hw wallet support on SafeOperator tx-service mode

830 of 951 branches covered (87.28%)

Branch coverage included in aggregate %.

53 of 66 new or added lines in 7 files covered. (80.3%)

1 existing line in 1 file now uncovered.

2859 of 3214 relevant lines covered (88.95%)

3.56 hits per line

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

81.9
/src/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
    SafeOperatorException,
57
    SafeVersionNotSupportedException,
58
    SameFallbackHandlerException,
59
    SameGuardException,
60
    SameMasterCopyException,
61
    SenderRequiredException,
62
    ThresholdLimitException,
63
    UpdateAddressesNotValid,
64
)
65
from safe_cli.safe_addresses import (
4✔
66
    get_default_fallback_handler_address,
67
    get_last_sign_message_lib_address,
68
    get_safe_contract_address,
69
    get_safe_l2_contract_address,
70
)
71
from safe_cli.utils import (
4✔
72
    choose_option_from_list,
73
    get_erc_20_list,
74
    get_input,
75
    yes_or_no_question,
76
)
77

78
from ..contracts import safe_to_l2_migration
4✔
79
from .hw_wallets.hw_wallet import HwWallet
4✔
80
from .hw_wallets.hw_wallet_manager import HwWalletType, get_hw_wallet_manager
4✔
81

82

83
@dataclasses.dataclass
4✔
84
class SafeCliInfo:
4✔
85
    address: str
4✔
86
    nonce: int
4✔
87
    threshold: int
4✔
88
    owners: List[str]
4✔
89
    master_copy: str
4✔
90
    modules: List[str]
4✔
91
    fallback_handler: str
4✔
92
    guard: str
4✔
93
    balance_ether: int
4✔
94
    version: str
4✔
95

96
    def __str__(self):
4✔
97
        return (
×
98
            f"safe-version={self.version} nonce={self.nonce} threshold={self.threshold} owners={self.owners} "
99
            f"master-copy={self.master_copy} fallback-hander={self.fallback_handler} "
100
            f"modules={self.modules} balance-ether={self.balance_ether:.4f}"
101
        )
102

103

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

120
    return decorated
4✔
121

122

123
def require_default_sender(f):
4✔
124
    """
125
    Throws SenderRequiredException if not default sender configured
126
    """
127

128
    @wraps(f)
4✔
129
    def decorated(self, *args, **kwargs):
4✔
130
        if not self.default_sender and not self.hw_wallet_manager.sender:
4✔
131
            raise SenderRequiredException()
4✔
132
        else:
133
            return f(self, *args, **kwargs)
4✔
134

135
    return decorated
4✔
136

137

138
class SafeOperator:
4✔
139
    address: ChecksumAddress
4✔
140
    node_url: str
4✔
141
    ethereum_client: EthereumClient
4✔
142
    ens: ENS
4✔
143
    network: EthereumNetwork
4✔
144
    etherscan: Optional[EtherscanClient]
4✔
145
    safe_tx_service: Optional[TransactionServiceApi]
4✔
146
    safe: Safe
4✔
147
    safe_contract: Contract
4✔
148
    safe_contract_1_1_0: Contract
4✔
149
    accounts: Set[LocalAccount] = set()
4✔
150
    default_sender: Optional[LocalAccount]
4✔
151
    executed_transactions: List[str]
4✔
152
    _safe_cli_info: Optional[SafeCliInfo]
4✔
153
    require_all_signatures: bool
4✔
154
    interactive: bool
4✔
155

156
    def __init__(
4✔
157
        self, address: ChecksumAddress, node_url: str, interactive: bool = True
158
    ):
159
        self.address = address
4✔
160
        self.node_url = node_url
4✔
161
        self.ethereum_client = EthereumClient(self.node_url)
4✔
162
        self.ens = ENS.from_web3(self.ethereum_client.w3)
4✔
163
        self.network: EthereumNetwork = self.ethereum_client.get_network()
4✔
164
        try:
4✔
165
            self.etherscan = EtherscanClient(self.network)
4✔
166
        except EtherscanClientConfigurationProblem:
4✔
167
            self.etherscan = None
4✔
168

169
        try:
4✔
170
            self.safe_tx_service = TransactionServiceApi.from_ethereum_client(
4✔
171
                self.ethereum_client
172
            )
173
        except EthereumNetworkNotSupported:
4✔
174
            self.safe_tx_service = None
4✔
175

176
        self.safe = Safe(address, self.ethereum_client)
4✔
177
        self.safe_contract = self.safe.contract
4✔
178
        self.safe_contract_1_1_0 = get_safe_V1_1_1_contract(
4✔
179
            self.ethereum_client.w3, address=self.address
180
        )
181
        self.accounts: Set[LocalAccount] = set()
4✔
182
        self.default_sender: Optional[LocalAccount] = None
4✔
183
        self.executed_transactions: List[str] = []
4✔
184
        self._safe_cli_info: Optional[SafeCliInfo] = None  # Cache for SafeCliInfo
4✔
185
        self.require_all_signatures = (
4✔
186
            True  # Require all signatures to be present to send a tx
187
        )
188
        self.hw_wallet_manager = get_hw_wallet_manager()
4✔
189
        self.interactive = interactive  # Disable prompt dialogs
4✔
190

191
    @cached_property
4✔
192
    def last_default_fallback_handler_address(self) -> ChecksumAddress:
4✔
193
        """
194
        :return: Address for last version of default fallback handler contract
195
        """
196
        return get_default_fallback_handler_address(self.ethereum_client)
×
197

198
    @cached_property
4✔
199
    def last_safe_contract_address(self) -> ChecksumAddress:
4✔
200
        """
201
        :return: Last version of the Safe Contract. Use events version for every network but mainnet
202
        """
203
        if self.network == EthereumNetwork.MAINNET:
4✔
204
            return get_safe_contract_address(self.ethereum_client)
×
205
        else:
206
            return get_safe_l2_contract_address(self.ethereum_client)
4✔
207

208
    @cached_property
4✔
209
    def ens_domain(self) -> Optional[str]:
4✔
210
        # FIXME After web3.py fixes the middleware copy
211
        if self.network == EthereumNetwork.MAINNET:
4✔
212
            return self.ens.name(self.address)
×
213

214
    @property
4✔
215
    def safe_cli_info(self) -> SafeCliInfo:
4✔
216
        if not self._safe_cli_info:
4✔
217
            self._safe_cli_info = self.refresh_safe_cli_info()
4✔
218
        return self._safe_cli_info
4✔
219

220
    def refresh_safe_cli_info(self) -> SafeCliInfo:
4✔
221
        self._safe_cli_info = self.get_safe_cli_info()
4✔
222
        return self._safe_cli_info
4✔
223

224
    def is_version_updated(self) -> bool:
4✔
225
        """
226
        :return: True if Safe Master Copy is updated, False otherwise
227
        """
228

229
        last_safe_contract_address = self.last_safe_contract_address
4✔
230
        if self.safe_cli_info.master_copy == last_safe_contract_address:
4✔
231
            return True
4✔
232
        else:  # Check versions, maybe safe-cli addresses were not updated
233
            try:
4✔
234
                safe_contract_version = Safe(
4✔
235
                    last_safe_contract_address, self.ethereum_client
236
                ).retrieve_version()
237
            except (
×
238
                BadFunctionCallOutput
239
            ):  # Safe master copy is not deployed or errored, maybe custom network
240
                return True  # We cannot say you are not updated ¯\_(ツ)_/¯
×
241

242
            return semantic_version.parse(
4✔
243
                self.safe_cli_info.version
244
            ) >= semantic_version.parse(safe_contract_version)
245

246
    def load_cli_owners_from_words(self, words: List[str]):
4✔
247
        if len(words) == 1:  # Reading seed from Environment Variable
×
248
            words = os.environ.get(words[0], default="").strip().split(" ")
×
249
        parsed_words = " ".join(words)
×
250
        try:
×
251
            for index in range(100):  # Try first accounts of seed phrase
×
252
                account = get_account_from_words(parsed_words, index=index)
×
253
                if account.address in self.safe_cli_info.owners:
×
254
                    self.load_cli_owners([account.key.hex()])
×
255
            if not index:
×
256
                print_formatted_text(
×
257
                    HTML(
258
                        "<ansired>Cannot generate any valid owner for this Safe</ansired>"
259
                    )
260
                )
261
        except ValidationError:
×
262
            print_formatted_text(
×
263
                HTML("<ansired>Cannot load owners from words</ansired>")
264
            )
265

266
    def load_cli_owners(self, keys: List[str]):
4✔
267
        for key in keys:
4✔
268
            try:
4✔
269
                account = Account.from_key(
4✔
270
                    os.environ.get(key, default=key)
271
                )  # Try to get key from `environ`
272
                self.accounts.add(account)
4✔
273
                balance = self.ethereum_client.get_balance(account.address)
4✔
274
                print_formatted_text(
4✔
275
                    HTML(
276
                        f"Loaded account <b>{account.address}</b> "
277
                        f'with balance={Web3.from_wei(balance, "ether")} ether'
278
                    )
279
                )
280
                if (
4✔
281
                    not self.default_sender
282
                    and not self.hw_wallet_manager.sender
283
                    and balance > 0
284
                ):
285
                    print_formatted_text(
4✔
286
                        HTML(
287
                            f"Set account <b>{account.address}</b> as default sender of txs"
288
                        )
289
                    )
290
                    self.default_sender = account
4✔
291
            except ValueError:
4✔
292
                if not self.interactive:
4✔
293
                    raise SafeOperatorException(f"Cannot load key={key}")
×
294
                print_formatted_text(HTML(f"<ansired>Cannot load key={key}</ansired>"))
4✔
295

296
    def load_hw_wallet(
4✔
297
        self,
298
        hw_wallet_type: HwWalletType,
299
        derivation_path: str,
300
        template_derivation_path: str,
301
    ):
302
        if not self.hw_wallet_manager.is_supported_hw_wallet(hw_wallet_type):
4✔
303
            return None
×
304
        if derivation_path is None:
4✔
305
            ledger_accounts = self.hw_wallet_manager.get_accounts(
4✔
306
                hw_wallet_type, template_derivation_path
307
            )
308
            if len(ledger_accounts) == 0:
4✔
309
                return None
4✔
310

311
            option = choose_option_from_list(
4✔
312
                "Select the owner address", ledger_accounts
313
            )
314
            if option is None:
4✔
315
                return None
×
316
            _, derivation_path = ledger_accounts[option]
4✔
317
        address = self.hw_wallet_manager.add_account(hw_wallet_type, derivation_path)
4✔
318
        balance = self.ethereum_client.get_balance(address)
4✔
319

320
        print_formatted_text(
4✔
321
            HTML(
322
                f"Loaded account <b>{address}</b> "
323
                f'with balance={Web3.from_wei(balance, "ether")} ether.'
324
            )
325
        )
326

327
        if (
4✔
328
            not self.default_sender
329
            and not self.hw_wallet_manager.sender
330
            and balance > 0
331
        ):
332
            self.hw_wallet_manager.set_sender(hw_wallet_type, derivation_path)
4✔
333
            print_formatted_text(HTML(f"HwDevice {address} added as sender"))
4✔
334
        else:
335
            print_formatted_text(HTML(f"HwDevice {address} wasn't added as sender"))
4✔
336

337
    def load_ledger_cli_owners(
4✔
338
        self, derivation_path: str = None, legacy_account: bool = False
339
    ):
340
        if legacy_account:
4✔
341
            self.load_hw_wallet(HwWalletType.LEDGER, derivation_path, "44'/60'/0'/{i}")
×
342
        else:
343
            self.load_hw_wallet(
4✔
344
                HwWalletType.LEDGER, derivation_path, "44'/60'/{i}'/0/0"
345
            )
346

347
    def load_trezor_cli_owners(
4✔
348
        self, derivation_path: str = None, legacy_account: bool = False
349
    ):
350
        if legacy_account:
×
351
            self.load_hw_wallet(HwWalletType.TREZOR, derivation_path, "44'/60'/0'/{i}")
×
352
        else:
353
            self.load_hw_wallet(
×
354
                HwWalletType.TREZOR, derivation_path, "44'/60'/0'/0/{i}"
355
            )
356

357
    def unload_cli_owners(self, owners: List[str]):
4✔
358
        accounts_to_remove: Set[Account] = set()
4✔
359
        for owner in owners:
4✔
360
            for account in self.accounts:
4✔
361
                if account.address == owner:
4✔
362
                    if self.default_sender and self.default_sender.address == owner:
4✔
363
                        self.default_sender = None
4✔
364
                    accounts_to_remove.add(account)
4✔
365
                    break
4✔
366
        self.accounts = self.accounts.difference(accounts_to_remove)
4✔
367
        # Check if there are ledger owners
368
        if self.hw_wallet_manager.wallets and len(accounts_to_remove) < len(owners):
4✔
369
            accounts_to_remove = (
4✔
370
                accounts_to_remove | self.hw_wallet_manager.delete_accounts(owners)
371
            )
372

373
        if accounts_to_remove:
4✔
374
            print_formatted_text(
4✔
375
                HTML("<ansigreen>Accounts have been deleted</ansigreen>")
376
            )
377
        else:
378
            print_formatted_text(HTML("<ansired>No account was deleted</ansired>"))
×
379

380
    def show_cli_owners(self):
4✔
381
        accounts = self.accounts | self.hw_wallet_manager.wallets
×
382
        if not accounts:
×
383
            print_formatted_text(HTML("<ansired>No accounts loaded</ansired>"))
×
384
        else:
385
            for account in accounts:
×
386
                print_formatted_text(
×
387
                    HTML(
388
                        f"<ansigreen><b>Account</b> {account.address} loaded</ansigreen>"
389
                    )
390
                )
391
            if self.default_sender:
×
392
                print_formatted_text(
×
393
                    HTML(
394
                        f"<ansigreen><b>Default sender:</b> {self.default_sender.address}"
395
                        f"</ansigreen>"
396
                    )
397
                )
398
            elif self.hw_wallet_manager.sender:
×
399
                print_formatted_text(
×
400
                    HTML(
401
                        f"<ansigreen><b>HwDevice sender:</b> {self.hw_wallet_manager.sender}"
402
                        f"</ansigreen>"
403
                    )
404
                )
405
            else:
406
                print_formatted_text(
×
407
                    HTML("<ansigreen>Not default sender set </ansigreen>")
408
                )
409

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

464
    def sign_message(
4✔
465
        self,
466
        eip712_message_path: Optional[str] = None,
467
    ) -> bool:
468
        if eip712_message_path:
4✔
469
            try:
4✔
470
                message = json.load(open(eip712_message_path, "r"))
4✔
471
                message_bytes = b"".join(eip712_encode(message))
4✔
472
            except ValueError:
×
473
                raise ValueError
×
474
        else:
475
            print_formatted_text("EIP191 message to sign:")
4✔
476
            message = get_input()
4✔
477
            message_bytes = message.encode("UTF-8")
4✔
478

479
        safe_message_hash = self.safe.get_message_hash(message_bytes)
4✔
480

481
        sign_message_lib_address = get_last_sign_message_lib_address(
4✔
482
            self.ethereum_client
483
        )
484
        contract = get_sign_message_lib_contract(self.ethereum_client.w3, self.address)
4✔
485
        sign_message_data = HexBytes(
4✔
486
            contract.functions.signMessage(message_bytes).build_transaction(
487
                get_empty_tx_params(),
488
            )["data"]
489
        )
490
        print_formatted_text(HTML(f"Signing message: \n {message}"))
4✔
491
        if self.prepare_and_execute_safe_transaction(
4✔
492
            sign_message_lib_address,
493
            0,
494
            sign_message_data,
495
            operation=SafeOperationEnum.DELEGATE_CALL,
496
        ):
497
            print_formatted_text(
4✔
498
                HTML(f"Message was signed correctly: {safe_message_hash.hex()}")
499
            )
500

501
    def confirm_message(self, safe_message_hash: bytes, sender: ChecksumAddress):
4✔
502
        return self._require_tx_service_mode()
×
503

504
    def add_owner(self, new_owner: str, threshold: Optional[int] = None) -> bool:
4✔
505
        threshold = threshold if threshold is not None else self.safe_cli_info.threshold
4✔
506
        if new_owner in self.safe_cli_info.owners:
4✔
507
            raise ExistingOwnerException(new_owner)
4✔
508
        else:
509
            # TODO Allow to set threshold
510
            transaction = self.safe_contract.functions.addOwnerWithThreshold(
4✔
511
                new_owner, threshold
512
            ).build_transaction({"from": self.address, "gas": 0, "gasPrice": 0})
513
            if self.execute_safe_internal_transaction(transaction["data"]):
4✔
514
                self.safe_cli_info.owners = self.safe.retrieve_owners()
4✔
515
                self.safe_cli_info.threshold = self.safe.retrieve_threshold()
4✔
516
                return True
4✔
517
            return False
×
518

519
    def remove_owner(self, owner_to_remove: str, threshold: Optional[int] = None):
4✔
520
        threshold = threshold if threshold is not None else self.safe_cli_info.threshold
4✔
521
        if owner_to_remove not in self.safe_cli_info.owners:
4✔
522
            raise NonExistingOwnerException(owner_to_remove)
4✔
523
        elif len(self.safe_cli_info.owners) == threshold:
4✔
524
            raise ThresholdLimitException()
×
525
        else:
526
            index_owner = self.safe_cli_info.owners.index(owner_to_remove)
4✔
527
            prev_owner = (
4✔
528
                self.safe_cli_info.owners[index_owner - 1]
529
                if index_owner
530
                else SENTINEL_ADDRESS
531
            )
532
            transaction = self.safe_contract.functions.removeOwner(
4✔
533
                prev_owner, owner_to_remove, threshold
534
            ).build_transaction({"from": self.address, "gas": 0, "gasPrice": 0})
535
            if self.execute_safe_internal_transaction(transaction["data"]):
4✔
536
                self.safe_cli_info.owners = self.safe.retrieve_owners()
4✔
537
                self.safe_cli_info.threshold = self.safe.retrieve_threshold()
4✔
538
                return True
4✔
539
            return False
×
540

541
    def send_custom(
4✔
542
        self,
543
        to: str,
544
        value: int,
545
        data: bytes,
546
        safe_nonce: Optional[int] = None,
547
        delegate_call: bool = False,
548
    ) -> bool:
549
        if value > 0:
4✔
550
            safe_balance = self.ethereum_client.get_balance(self.address)
4✔
551
            if safe_balance < value:
4✔
552
                raise NotEnoughEtherToSend(safe_balance)
4✔
553
        operation = (
4✔
554
            SafeOperationEnum.DELEGATE_CALL if delegate_call else SafeOperationEnum.CALL
555
        )
556
        return self.prepare_and_execute_safe_transaction(
4✔
557
            to, value, data, operation, safe_nonce=safe_nonce
558
        )
559

560
    def send_ether(self, to: str, value: int, **kwargs) -> bool:
4✔
561
        return self.send_custom(to, value, b"", **kwargs)
4✔
562

563
    def send_erc20(self, to: str, token_address: str, amount: int, **kwargs) -> bool:
4✔
564
        transaction = (
4✔
565
            get_erc20_contract(self.ethereum_client.w3, token_address)
566
            .functions.transfer(to, amount)
567
            .build_transaction({"from": self.address, "gas": 0, "gasPrice": 0})
568
        )
569
        return self.send_custom(
4✔
570
            token_address, 0, HexBytes(transaction["data"]), **kwargs
571
        )
572

573
    def send_erc721(self, to: str, token_address: str, token_id: int, **kwargs) -> bool:
4✔
574
        transaction = (
4✔
575
            get_erc721_contract(self.ethereum_client.w3, token_address)
576
            .functions.transferFrom(self.address, to, token_id)
577
            .build_transaction({"from": self.address, "gas": 0, "gasPrice": 0})
578
        )
579
        return self.send_custom(token_address, 0, transaction["data"], **kwargs)
4✔
580

581
    def change_fallback_handler(self, new_fallback_handler: str) -> bool:
4✔
582
        if new_fallback_handler == self.safe_cli_info.fallback_handler:
4✔
583
            raise SameFallbackHandlerException(new_fallback_handler)
4✔
584
        elif semantic_version.parse(
4✔
585
            self.safe_cli_info.version
586
        ) < semantic_version.parse("1.1.0"):
587
            raise FallbackHandlerNotSupportedException()
4✔
588
        elif (
4✔
589
            new_fallback_handler != NULL_ADDRESS
590
            and not self.ethereum_client.is_contract(new_fallback_handler)
591
        ):
592
            raise InvalidFallbackHandlerException(
4✔
593
                f"{new_fallback_handler} address is not a contract"
594
            )
595
        else:
596
            transaction = self.safe_contract.functions.setFallbackHandler(
4✔
597
                new_fallback_handler
598
            ).build_transaction({"from": self.address, "gas": 0, "gasPrice": 0})
599
            if self.execute_safe_internal_transaction(transaction["data"]):
4✔
600
                self.safe_cli_info.fallback_handler = new_fallback_handler
4✔
601
                self.safe_cli_info.version = self.safe.retrieve_version()
4✔
602
                return True
4✔
603

604
    def change_guard(self, guard: str) -> bool:
4✔
605
        if guard == self.safe_cli_info.guard:
4✔
606
            raise SameGuardException(guard)
4✔
607
        elif semantic_version.parse(
4✔
608
            self.safe_cli_info.version
609
        ) < semantic_version.parse("1.3.0"):
610
            raise GuardNotSupportedException()
4✔
611
        elif guard != NULL_ADDRESS and not self.ethereum_client.is_contract(guard):
4✔
612
            raise InvalidGuardException(f"{guard} address is not a contract")
4✔
613
        else:
614
            transaction = self.safe_contract.functions.setGuard(
4✔
615
                guard
616
            ).build_transaction({"from": self.address, "gas": 0, "gasPrice": 0})
617
            if self.execute_safe_internal_transaction(transaction["data"]):
4✔
618
                self.safe_cli_info.guard = guard
4✔
619
                self.safe_cli_info.version = self.safe.retrieve_version()
4✔
620
                return True
4✔
621

622
    def change_master_copy(self, new_master_copy: str) -> bool:
4✔
623
        if new_master_copy == self.safe_cli_info.master_copy:
4✔
624
            raise SameMasterCopyException(new_master_copy)
4✔
625
        else:
626
            safe_version = self.safe.retrieve_version()
4✔
627
            if semantic_version.parse(safe_version) >= semantic_version.parse("1.3.0"):
4✔
628
                raise SafeVersionNotSupportedException(
4✔
629
                    f"{safe_version} cannot be updated (yet)"
630
                )
631

632
            try:
4✔
633
                Safe(new_master_copy, self.ethereum_client).retrieve_version()
4✔
634
            except BadFunctionCallOutput:
4✔
635
                raise InvalidMasterCopyException(new_master_copy)
4✔
636

637
            transaction = self.safe_contract_1_1_0.functions.changeMasterCopy(
4✔
638
                new_master_copy
639
            ).build_transaction({"from": self.address, "gas": 0, "gasPrice": 0})
640
            if self.execute_safe_internal_transaction(transaction["data"]):
4✔
641
                self.safe_cli_info.master_copy = new_master_copy
4✔
642
                self.safe_cli_info.version = self.safe.retrieve_version()
4✔
643
                return True
4✔
644

645
    def update_version(self) -> Optional[bool]:
4✔
646
        """
647
        Update Safe Master Copy and Fallback handler to the last version
648

649
        :return:
650
        """
651

652
        safe_version = self.safe.retrieve_version()
4✔
653
        if semantic_version.parse(safe_version) >= semantic_version.parse("1.3.0"):
4✔
654
            raise SafeVersionNotSupportedException(
4✔
655
                f"{safe_version} cannot be updated (yet)"
656
            )
657

658
        if self.is_version_updated():
4✔
659
            raise SafeAlreadyUpdatedException(f"{safe_version} already updated")
×
660

661
        addresses = (
4✔
662
            self.last_safe_contract_address,
663
            self.last_default_fallback_handler_address,
664
        )
665
        if not all(
4✔
666
            self.ethereum_client.is_contract(contract) for contract in addresses
667
        ):
668
            raise UpdateAddressesNotValid(
×
669
                "Not valid addresses to update Safe", *addresses
670
            )
671

672
        multisend = MultiSend(ethereum_client=self.ethereum_client)
4✔
673
        tx_params = {"from": self.address, "gas": 0, "gasPrice": 0}
4✔
674
        multisend_txs = [
4✔
675
            MultiSendTx(MultiSendOperation.CALL, self.address, 0, data)
676
            for data in (
677
                self.safe_contract_1_1_0.functions.changeMasterCopy(
678
                    self.last_safe_contract_address
679
                ).build_transaction(tx_params)["data"],
680
                self.safe_contract_1_1_0.functions.setFallbackHandler(
681
                    self.last_default_fallback_handler_address
682
                ).build_transaction(tx_params)["data"],
683
            )
684
        ]
685

686
        multisend_data = multisend.build_tx_data(multisend_txs)
4✔
687

688
        if self.prepare_and_execute_safe_transaction(
4✔
689
            multisend.address,
690
            0,
691
            multisend_data,
692
            operation=SafeOperationEnum.DELEGATE_CALL,
693
        ):
694
            self.safe_cli_info.master_copy = self.last_safe_contract_address
4✔
695
            self.safe_cli_info.fallback_handler = (
4✔
696
                self.last_default_fallback_handler_address
697
            )
698
            self.safe_cli_info.version = self.safe.retrieve_version()
4✔
699

700
    def update_version_to_l2(
4✔
701
        self, migration_contract_address: ChecksumAddress
702
    ) -> Optional[bool]:
703
        """
704
        Update not L2 Safe to L2, so official UI supports it. Useful when replaying Safes deployed in
705
        non L2 networks (like mainnet) in L2 networks.
706
        Only v1.1.1, v1.3.0 and v1.4.1 versions are supported. Also, Safe nonce must be 0.
707

708
        :return:
709
        """
710

711
        if not self.ethereum_client.is_contract(migration_contract_address):
4✔
712
            raise InvalidMigrationContractException(
×
713
                f"Non L2 to L2 migration contract {migration_contract_address} is not deployed"
714
            )
715

716
        safe_version = self.safe.retrieve_version()
4✔
717
        chain_id = self.ethereum_client.get_chain_id()
4✔
718

719
        if self.safe.retrieve_nonce() > 0:
4✔
720
            raise InvalidNonceException("Nonce must be 0 for non L2 to L2 migration")
×
721

722
        l2_migration_contract = self.ethereum_client.w3.eth.contract(
4✔
723
            NULL_ADDRESS, abi=safe_to_l2_migration["abi"]
724
        )
725
        if safe_version == "1.1.1":
4✔
726
            safe_l2_singleton = safe_deployments["1.3.0"]["GnosisSafeL2"][str(chain_id)]
4✔
727
            fallback_handler = safe_deployments["1.3.0"][
4✔
728
                "CompatibilityFallbackHandler"
729
            ][str(chain_id)]
730
            data = HexBytes(
4✔
731
                l2_migration_contract.functions.migrateFromV111(
732
                    safe_l2_singleton, fallback_handler
733
                ).build_transaction(get_empty_tx_params())["data"]
734
            )
735
        elif safe_version in ("1.3.0", "1.4.1"):
4✔
736
            safe_l2_singleton = safe_deployments[safe_version]["GnosisSafeL2"][
4✔
737
                str(chain_id)
738
            ]
739
            fallback_handler = self.safe_cli_info.fallback_handler
4✔
740
            data = HexBytes(
4✔
741
                l2_migration_contract.functions.migrateToL2(
742
                    safe_l2_singleton
743
                ).build_transaction(get_empty_tx_params())["data"]
744
            )
745
        else:
746
            raise InvalidMasterCopyException(
×
747
                "Current version is not supported to migrate to L2"
748
            )
749

750
        if self.prepare_and_execute_safe_transaction(
4✔
751
            migration_contract_address,
752
            0,
753
            data,
754
            operation=SafeOperationEnum.DELEGATE_CALL,
755
        ):
756
            self.safe_cli_info.master_copy = safe_l2_singleton
4✔
757
            self.safe_cli_info.fallback_handler = fallback_handler
4✔
758
            self.safe_cli_info.version = self.safe.retrieve_version()
4✔
759

760
    def change_threshold(self, threshold: int):
4✔
761
        if threshold == self.safe_cli_info.threshold:
4✔
762
            print_formatted_text(
×
763
                HTML(f"<ansired>Threshold is already {threshold}</ansired>")
764
            )
765
        elif threshold > len(self.safe_cli_info.owners):
4✔
766
            print_formatted_text(
×
767
                HTML(
768
                    f"<ansired>Threshold={threshold} bigger than number "
769
                    f"of owners={len(self.safe_cli_info.owners)}</ansired>"
770
                )
771
            )
772
        else:
773
            transaction = self.safe_contract.functions.changeThreshold(
4✔
774
                threshold
775
            ).build_transaction({"from": self.address, "gas": 0, "gasPrice": 0})
776

777
            if self.execute_safe_internal_transaction(transaction["data"]):
4✔
778
                self.safe_cli_info.threshold = self.safe.retrieve_threshold()
4✔
779

780
    def enable_module(self, module_address: str):
4✔
781
        if module_address in self.safe_cli_info.modules:
×
782
            print_formatted_text(
×
783
                HTML(f"<ansired>Module {module_address} is already enabled</ansired>")
784
            )
785
        else:
786
            transaction = self.safe_contract.functions.enableModule(
×
787
                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 disable_module(self, module_address: str):
4✔
793
        if module_address not in self.safe_cli_info.modules:
×
794
            print_formatted_text(
×
795
                HTML(f"<ansired>Module {module_address} is not enabled</ansired>")
796
            )
797
        else:
798
            pos = self.safe_cli_info.modules.index(module_address)
×
799
            if pos == 0:
×
800
                previous_address = SENTINEL_ADDRESS
×
801
            else:
802
                previous_address = self.safe_cli_info.modules[pos - 1]
×
803
            transaction = self.safe_contract.functions.disableModule(
×
804
                previous_address, module_address
805
            ).build_transaction({"from": self.address, "gas": 0, "gasPrice": 0})
806
            if self.execute_safe_internal_transaction(transaction["data"]):
×
807
                self.safe_cli_info.modules = self.safe.retrieve_modules()
×
808

809
    def print_info(self):
4✔
810
        for key, value in dataclasses.asdict(self.safe_cli_info).items():
4✔
811
            print_formatted_text(
4✔
812
                HTML(
813
                    f"<b><ansigreen>{key.capitalize()}</ansigreen></b>="
814
                    f"<ansiblue>{value}</ansiblue>"
815
                )
816
            )
817
        if self.ens_domain:
4✔
818
            print_formatted_text(
×
819
                HTML(
820
                    f"<b><ansigreen>Ens domain</ansigreen></b>="
821
                    f"<ansiblue>{self.ens_domain}</ansiblue>"
822
                )
823
            )
824
        if self.safe_tx_service:
4✔
825
            url = f"{self.safe_tx_service.base_url}/api/v1/safes/{self.address}/transactions/"
×
826
            print_formatted_text(
×
827
                HTML(
828
                    f"<b><ansigreen>Safe Tx Service</ansigreen></b>="
829
                    f"<ansiblue>{url}</ansiblue>"
830
                )
831
            )
832

833
        if self.etherscan:
4✔
834
            url = f"{self.etherscan.base_url}/address/{self.address}"
×
835
            print_formatted_text(
×
836
                HTML(
837
                    f"<b><ansigreen>Etherscan</ansigreen></b>="
838
                    f"<ansiblue>{url}</ansiblue>"
839
                )
840
            )
841

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

857
        if not self.hw_wallet_manager.is_supported_hw_wallet(HwWalletType.TREZOR):
4✔
858
            print_formatted_text(
×
859
                HTML(
860
                    "<b><ansigreen>Trezor</ansigreen></b>="
861
                    "<ansired>Disabled </ansired> <b>Optional trezor library is not installed, run pip install safe-cli[trezor] </b>"
862
                )
863
            )
864
        else:
865
            print_formatted_text(
4✔
866
                HTML(
867
                    "<b><ansigreen>Trezor</ansigreen></b>="
868
                    "<ansiblue>supported</ansiblue>"
869
                )
870
            )
871

872
        if not self.is_version_updated():
4✔
873
            print_formatted_text(
×
874
                HTML(
875
                    "<ansired>Safe is not updated! You can use <b>update</b> command to update "
876
                    "the Safe to a newest version</ansired>"
877
                )
878
            )
879

880
    def get_safe_cli_info(self) -> SafeCliInfo:
4✔
881
        safe = self.safe
4✔
882
        balance_ether = Web3.from_wei(
4✔
883
            self.ethereum_client.get_balance(self.address), "ether"
884
        )
885
        safe_info = safe.retrieve_all_info()
4✔
886
        return SafeCliInfo(
4✔
887
            self.address,
888
            safe_info.nonce,
889
            safe_info.threshold,
890
            safe_info.owners,
891
            safe_info.master_copy,
892
            safe_info.modules,
893
            safe_info.fallback_handler,
894
            safe_info.guard,
895
            balance_ether,
896
            safe_info.version,
897
        )
898

899
    def get_threshold(self):
4✔
900
        print_formatted_text(self.safe.retrieve_threshold())
×
901

902
    def get_nonce(self):
4✔
903
        print_formatted_text(self.safe.retrieve_nonce())
4✔
904

905
    def get_owners(self):
4✔
906
        print_formatted_text(self.safe.retrieve_owners())
×
907

908
    def execute_safe_internal_transaction(self, data: bytes) -> bool:
4✔
909
        return self.prepare_and_execute_safe_transaction(self.address, 0, data)
4✔
910

911
    def prepare_safe_transaction(
4✔
912
        self,
913
        to: str,
914
        value: int,
915
        data: bytes,
916
        operation: SafeOperationEnum = SafeOperationEnum.CALL,
917
        safe_nonce: Optional[int] = None,
918
    ) -> SafeTx:
919
        safe_tx = self.safe.build_multisig_tx(
4✔
920
            to, value, data, operation=operation.value, safe_nonce=safe_nonce
921
        )
922
        self.sign_transaction(safe_tx)  # Raises exception if it cannot be signed
4✔
923
        return safe_tx
4✔
924

925
    def prepare_and_execute_safe_transaction(
4✔
926
        self,
927
        to: str,
928
        value: int,
929
        data: bytes,
930
        operation: SafeOperationEnum = SafeOperationEnum.CALL,
931
        safe_nonce: Optional[int] = None,
932
    ) -> bool:
933
        safe_tx = self.prepare_safe_transaction(
4✔
934
            to, value, data, operation, safe_nonce=safe_nonce
935
        )
936
        return self.execute_safe_transaction(safe_tx)
4✔
937

938
    @require_default_sender  # Throws Exception if default sender not found
4✔
939
    def execute_safe_transaction(self, safe_tx: SafeTx):
4✔
940
        try:
4✔
941
            if self.default_sender:
4✔
942
                call_result = safe_tx.call(self.default_sender.address)
4✔
943
            else:
944
                call_result = safe_tx.call(self.hw_wallet_manager.sender.address)
×
945
            print_formatted_text(HTML(f"Result: <ansigreen>{call_result}</ansigreen>"))
4✔
946
            if not self.interactive or yes_or_no_question(
4✔
947
                "Do you want to execute tx " + str(safe_tx)
948
            ):
949
                if self.default_sender:
4✔
950
                    tx_hash, tx = safe_tx.execute(
4✔
951
                        self.default_sender.key, eip1559_speed=TxSpeed.NORMAL
952
                    )
953
                else:
954
                    tx_hash, tx = self.hw_wallet_manager.execute_safe_tx(
×
955
                        safe_tx, eip1559_speed=TxSpeed.NORMAL
956
                    )
957
                self.executed_transactions.append(tx_hash.hex())
4✔
958
                print_formatted_text(
4✔
959
                    HTML(
960
                        f"<ansigreen>Sent tx with tx-hash {tx_hash.hex()} "
961
                        f"and safe-nonce {safe_tx.safe_nonce}, waiting for receipt</ansigreen>"
962
                    )
963
                )
964
                tx_receipt = self.ethereum_client.get_transaction_receipt(
4✔
965
                    tx_hash, timeout=120
966
                )
967
                if tx_receipt:
4✔
968
                    fees = self.ethereum_client.w3.from_wei(
4✔
969
                        tx_receipt["gasUsed"]
970
                        * tx_receipt.get("effectiveGasPrice", tx.get("gasPrice", 0)),
971
                        "ether",
972
                    )
973
                    print_formatted_text(
4✔
974
                        HTML(
975
                            f"<ansigreen>Tx was executed on block-number={tx_receipt['blockNumber']}, fees "
976
                            f"deducted={fees}</ansigreen>"
977
                        )
978
                    )
979
                    self.safe_cli_info.nonce += 1
4✔
980
                    return True
4✔
981
                else:
982
                    print_formatted_text(
×
983
                        HTML(
984
                            f"<ansired>Tx with tx-hash {tx_hash.hex()} still not mined</ansired>"
985
                        )
986
                    )
987
        except InvalidInternalTx as invalid_internal_tx:
4✔
988
            print_formatted_text(
×
989
                HTML(f"Result: <ansired>InvalidTx - {invalid_internal_tx}</ansired>")
990
            )
991
        return False
×
992

993
    # Batch_transactions multisend
994
    def batch_safe_txs(
4✔
995
        self, safe_nonce: int, safe_txs: Sequence[SafeTx]
996
    ) -> Optional[SafeTx]:
997
        """
998
        Submit signatures to the tx service. It's recommended to be on Safe v1.3.0 to prevent issues
999
        with `safeTxGas` and gas estimation.
1000

1001
        :return:
1002
        """
1003

1004
        try:
4✔
1005
            multisend = MultiSend(ethereum_client=self.ethereum_client)
4✔
1006
        except ValueError:
4✔
1007
            if not self.interactive:
4✔
1008
                raise SafeOperatorException(
4✔
1009
                    "Multisend contract is not deployed on this network and it's required for batching txs"
1010
                )
1011
            multisend = None
×
1012
            print_formatted_text(
×
1013
                HTML(
1014
                    "<ansired>Multisend contract is not deployed on this network and it's required for "
1015
                    "batching txs</ansired>"
1016
                )
1017
            )
1018

1019
        multisend_txs = []
4✔
1020
        for safe_tx in safe_txs:
4✔
1021
            # Check if call is already a Multisend call
1022
            inner_txs = MultiSend.from_transaction_data(safe_tx.data)
4✔
1023
            if inner_txs:
4✔
1024
                multisend_txs.extend(inner_txs)
×
1025
            else:
1026
                multisend_txs.append(
4✔
1027
                    MultiSendTx(
1028
                        MultiSendOperation.CALL, safe_tx.to, safe_tx.value, safe_tx.data
1029
                    )
1030
                )
1031

1032
        if len(multisend_txs) == 1:
4✔
1033
            safe_tx.safe_tx_gas = 0
×
1034
            safe_tx.base_gas = 0
×
1035
            safe_tx.gas_price = 0
×
1036
            safe_tx.signatures = b""
×
1037
            safe_tx.safe_nonce = safe_nonce  # Resend single transaction
×
1038
        elif multisend:
4✔
1039
            safe_tx = SafeTx(
4✔
1040
                self.ethereum_client,
1041
                self.address,
1042
                multisend.address,
1043
                0,
1044
                multisend.build_tx_data(multisend_txs),
1045
                SafeOperationEnum.DELEGATE_CALL.value,
1046
                0,
1047
                0,
1048
                0,
1049
                None,
1050
                None,
1051
                safe_nonce=safe_nonce,
1052
            )
1053
        else:
1054
            # Multisend not defined
1055
            return None
×
1056

1057
        safe_tx = self.sign_transaction(safe_tx)
4✔
1058
        if not safe_tx.signatures:
4✔
1059
            print_formatted_text(
×
1060
                HTML("<ansired>At least one owner must be loaded</ansired>")
1061
            )
1062
            return None
×
1063
        else:
1064
            return safe_tx
4✔
1065

1066
    def get_signers(self) -> Tuple[List[LocalAccount], List[HwWallet]]:
4✔
1067
        """
1068
        Get the signers necessary to sign a transaction, raise an exception if was not uploaded enough signers.
1069

1070
        :return: Tuple with eoa signers and hw_wallet signers
1071
        """
1072
        permitted_signers = self.get_permitted_signers()
4✔
1073
        threshold = self.safe_cli_info.threshold
4✔
1074
        eoa_signers: List[Account] = (
4✔
1075
            []
1076
        )  # Some accounts that are not an owner can be loaded
1077
        for account in self.accounts:
4✔
1078
            if account.address in permitted_signers:
4✔
1079
                eoa_signers.append(account)
4✔
1080
                threshold -= 1
4✔
1081
                if threshold == 0:
4✔
1082
                    break
4✔
1083
        # If still pending required signatures continue with ledger owners
1084
        hw_wallet_signers = []
4✔
1085
        if threshold > 0 and self.hw_wallet_manager.wallets:
4✔
1086
            for hw_wallet in self.hw_wallet_manager.wallets:
4✔
1087
                if hw_wallet.address in permitted_signers:
4✔
1088
                    hw_wallet_signers.append(hw_wallet)
4✔
1089
                    threshold -= 1
4✔
1090
                    if threshold == 0:
4✔
1091
                        break
4✔
1092

1093
        if self.require_all_signatures and threshold > 0:
4✔
1094
            raise NotEnoughSignatures(threshold)
4✔
1095

1096
        return (eoa_signers, hw_wallet_signers)
4✔
1097

1098
    # TODO Set sender so we can save gas in that signature
1099
    def sign_transaction(self, safe_tx: SafeTx) -> SafeTx:
4✔
1100
        eoa_signers, hw_wallets_signers = self.get_signers()
4✔
1101
        for selected_account in eoa_signers:
4✔
1102
            safe_tx.sign(selected_account.key)
4✔
1103

1104
        # Sign with ledger
1105
        if len(hw_wallets_signers):
4✔
1106
            safe_tx = self.hw_wallet_manager.sign_safe_tx(safe_tx, hw_wallets_signers)
4✔
1107

1108
        return safe_tx
4✔
1109

1110
    @require_tx_service
4✔
1111
    def _require_tx_service_mode(self):
4✔
1112
        print_formatted_text(
×
1113
            HTML(
1114
                "<ansired>First enter tx-service mode using <b>tx-service</b> command</ansired>"
1115
            )
1116
        )
1117

1118
    def get_delegates(self):
4✔
1119
        return self._require_tx_service_mode()
×
1120

1121
    def add_delegate(self, delegate_address: str, label: str, signer_address: str):
4✔
1122
        return self._require_tx_service_mode()
×
1123

1124
    def remove_delegate(self, delegate_address: str, signer_address: str):
4✔
1125
        return self._require_tx_service_mode()
×
1126

1127
    def submit_signatures(self, safe_tx_hash: bytes) -> bool:
4✔
1128
        return self._require_tx_service_mode()
×
1129

1130
    def get_balances(self):
4✔
1131
        return self._require_tx_service_mode()
×
1132

1133
    def get_transaction_history(self):
4✔
1134
        return self._require_tx_service_mode()
×
1135

1136
    def batch_txs(self, safe_nonce: int, safe_tx_hashes: Sequence[bytes]) -> bool:
4✔
1137
        return self._require_tx_service_mode()
×
1138

1139
    def execute_tx(self, safe_tx_hash: Sequence[bytes]) -> bool:
4✔
1140
        return self._require_tx_service_mode()
×
1141

1142
    def get_permitted_signers(self) -> Set[ChecksumAddress]:
4✔
1143
        """
1144
        :return: Accounts that can sign a transaction
1145
        """
1146
        return set(self.safe_cli_info.owners)
4✔
1147

1148
    def drain(self, to: str):
4✔
1149
        # Getting all events related with ERC20 transfers
1150
        last = self.ethereum_client.get_block("latest")["number"]
4✔
1151
        token_addresses = get_erc_20_list(self.ethereum_client, self.address, 1, last)
4✔
1152
        safe_txs = []
4✔
1153
        for token_address in token_addresses:
4✔
1154
            balance = self.ethereum_client.erc20.get_balance(
4✔
1155
                self.address, token_address
1156
            )
1157
            if balance > 0:
4✔
1158
                transaction = (
4✔
1159
                    get_erc20_contract(self.ethereum_client.w3, token_address)
1160
                    .functions.transfer(to, balance)
1161
                    .build_transaction({"from": self.address, "gas": 0, "gasPrice": 0})
1162
                )
1163

1164
                safe_tx = self.prepare_safe_transaction(
4✔
1165
                    token_address,
1166
                    0,
1167
                    HexBytes(transaction["data"]),
1168
                    SafeOperationEnum.CALL,
1169
                    safe_nonce=None,
1170
                )
1171
                safe_txs.append(safe_tx)
4✔
1172

1173
        # Getting ethereum balance
1174
        balance_eth = self.ethereum_client.get_balance(self.address)
4✔
1175
        if balance_eth:
4✔
1176
            safe_tx = self.prepare_safe_transaction(
4✔
1177
                to,
1178
                balance_eth,
1179
                b"",
1180
                SafeOperationEnum.CALL,
1181
                safe_nonce=None,
1182
            )
1183
            safe_txs.append(safe_tx)
4✔
1184

1185
        if safe_txs:
4✔
1186
            multisend_tx = self.batch_safe_txs(self.get_nonce(), safe_txs)
4✔
1187
            if multisend_tx is not None:
4✔
1188
                if self.execute_safe_transaction(multisend_tx):
4✔
1189
                    print_formatted_text(
4✔
1190
                        HTML(
1191
                            "<ansigreen>Transaction to drain account correctly executed</ansigreen>"
1192
                        )
1193
                    )
1194
        else:
1195
            print_formatted_text(
×
1196
                HTML("<ansigreen>Safe account is currently empty</ansigreen>")
1197
            )
1198

1199
    def remove_proposed_transaction(self, safe_tx_hash: bytes):
4✔
1200
        return self._require_tx_service_mode()
×
1201

1202
    def process_command(self, first_command: str, rest_command: List[str]) -> bool:
4✔
1203
        if first_command == "help":
×
1204
            print_formatted_text("I still cannot help you")
×
1205
        elif first_command == "refresh":
×
1206
            print_formatted_text("Reloading Safe information")
×
1207
            self.refresh_safe_cli_info()
×
1208

1209
        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

© 2025 Coveralls, Inc