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

safe-global / safe-cli / 7874176352

12 Feb 2024 03:39PM UTC coverage: 95.086%. Remained the same
7874176352

push

github

Uxio0
Bump web3 from 6.15.0 to 6.15.1

Bumps [web3](https://github.com/ethereum/web3.py) from 6.15.0 to 6.15.1.
- [Changelog](https://github.com/ethereum/web3.py/blob/v6.15.1/docs/releases.rst)
- [Commits](https://github.com/ethereum/web3.py/compare/v6.15.0...v6.15.1)

---
updated-dependencies:
- dependency-name: web3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

2109 of 2218 relevant lines covered (95.09%)

3.75 hits per line

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

95.88
/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, SafeOperation, 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=SafeOperation.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 = SafeOperation.DELEGATE_CALL if delegate_call else SafeOperation.CALL
4✔
537
        return self.prepare_and_execute_safe_transaction(
4✔
538
            to, value, data, operation, safe_nonce=safe_nonce
4✔
539
        )
540

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

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

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

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

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

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

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

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

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

630
        :return:
631
        """
632

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

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

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

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

667
        multisend_data = multisend.build_tx_data(multisend_txs)
4✔
668

669
        if self.prepare_and_execute_safe_transaction(
4✔
670
            multisend.address, 0, multisend_data, operation=SafeOperation.DELEGATE_CALL
4✔
671
        ):
672
            self.safe_cli_info.master_copy = self.last_safe_contract_address
4✔
673
            self.safe_cli_info.fallback_handler = (
4✔
674
                self.last_default_fallback_handler_address
4✔
675
            )
676
            self.safe_cli_info.version = self.safe.retrieve_version()
4✔
677

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

686
        :return:
687
        """
688

689
        if not self.ethereum_client.is_contract(migration_contract_address):
4✔
690
            raise InvalidMigrationContractException(
691
                f"Non L2 to L2 migration contract {migration_contract_address} is not deployed"
692
            )
693

694
        safe_version = self.safe.retrieve_version()
4✔
695
        chain_id = self.ethereum_client.get_chain_id()
4✔
696

697
        if self.safe.retrieve_nonce() > 0:
4✔
698
            raise InvalidNonceException("Nonce must be 0 for non L2 to L2 migration")
699

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

728
        if self.prepare_and_execute_safe_transaction(
4✔
729
            migration_contract_address, 0, data, operation=SafeOperation.DELEGATE_CALL
4✔
730
        ):
731
            self.safe_cli_info.master_copy = safe_l2_singleton
4✔
732
            self.safe_cli_info.fallback_handler = fallback_handler
4✔
733
            self.safe_cli_info.version = self.safe.retrieve_version()
4✔
734

735
    def change_threshold(self, threshold: int):
4✔
736
        if threshold == self.safe_cli_info.threshold:
4✔
737
            print_formatted_text(
738
                HTML(f"<ansired>Threshold is already {threshold}</ansired>")
739
            )
740
        elif threshold > len(self.safe_cli_info.owners):
4✔
741
            print_formatted_text(
742
                HTML(
743
                    f"<ansired>Threshold={threshold} bigger than number "
744
                    f"of owners={len(self.safe_cli_info.owners)}</ansired>"
745
                )
746
            )
747
        else:
748
            transaction = self.safe_contract.functions.changeThreshold(
4✔
749
                threshold
4✔
750
            ).build_transaction({"from": self.address, "gas": 0, "gasPrice": 0})
4✔
751

752
            if self.execute_safe_internal_transaction(transaction["data"]):
4✔
753
                self.safe_cli_info.threshold = threshold
4✔
754

755
    def enable_module(self, module_address: str):
4✔
756
        if module_address in self.safe_cli_info.modules:
757
            print_formatted_text(
758
                HTML(f"<ansired>Module {module_address} is already enabled</ansired>")
759
            )
760
        else:
761
            transaction = self.safe_contract.functions.enableModule(
762
                module_address
763
            ).build_transaction({"from": self.address, "gas": 0, "gasPrice": 0})
764
            if self.execute_safe_internal_transaction(transaction["data"]):
765
                self.safe_cli_info.modules = self.safe.retrieve_modules()
766

767
    def disable_module(self, module_address: str):
4✔
768
        if module_address not in self.safe_cli_info.modules:
769
            print_formatted_text(
770
                HTML(f"<ansired>Module {module_address} is not enabled</ansired>")
771
            )
772
        else:
773
            pos = self.safe_cli_info.modules.index(module_address)
774
            if pos == 0:
775
                previous_address = SENTINEL_ADDRESS
776
            else:
777
                previous_address = self.safe_cli_info.modules[pos - 1]
778
            transaction = self.safe_contract.functions.disableModule(
779
                previous_address, module_address
780
            ).build_transaction({"from": self.address, "gas": 0, "gasPrice": 0})
781
            if self.execute_safe_internal_transaction(transaction["data"]):
782
                self.safe_cli_info.modules = self.safe.retrieve_modules()
783

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

808
        if self.etherscan:
4✔
809
            url = f"{self.etherscan.base_url}/address/{self.address}"
4✔
810
            print_formatted_text(
4✔
811
                HTML(
4✔
812
                    f"<b><ansigreen>Etherscan</ansigreen></b>="
4✔
813
                    f"<ansiblue>{url}</ansiblue>"
3✔
814
                )
815
            )
816

817
        if not self.hw_wallet_manager.is_supported_hw_wallet(HwWalletType.LEDGER):
4✔
818
            print_formatted_text(
819
                HTML(
820
                    "<b><ansigreen>Ledger</ansigreen></b>="
821
                    "<ansired>Disabled </ansired> <b>Optional ledger library is not installed, run pip install safe-cli[ledger] </b>"
822
                )
823
            )
824
        else:
825
            print_formatted_text(
4✔
826
                HTML(
4✔
827
                    "<b><ansigreen>Ledger</ansigreen></b>="
4✔
828
                    "<ansiblue>supported</ansiblue>"
829
                )
830
            )
831

832
        if not self.hw_wallet_manager.is_supported_hw_wallet(HwWalletType.TREZOR):
4✔
833
            print_formatted_text(
834
                HTML(
835
                    "<b><ansigreen>Trezor</ansigreen></b>="
836
                    "<ansired>Disabled </ansired> <b>Optional trezor library is not installed, run pip install safe-cli[trezor] </b>"
837
                )
838
            )
839
        else:
840
            print_formatted_text(
4✔
841
                HTML(
4✔
842
                    "<b><ansigreen>Trezor</ansigreen></b>="
4✔
843
                    "<ansiblue>supported</ansiblue>"
844
                )
845
            )
846

847
        if not self.is_version_updated():
4✔
848
            print_formatted_text(
849
                HTML(
850
                    "<ansired>Safe is not updated! You can use <b>update</b> command to update "
851
                    "the Safe to a newest version</ansired>"
852
                )
853
            )
854

855
    def get_safe_cli_info(self) -> SafeCliInfo:
4✔
856
        safe = self.safe
4✔
857
        balance_ether = Web3.from_wei(
4✔
858
            self.ethereum_client.get_balance(self.address), "ether"
4✔
859
        )
860
        safe_info = safe.retrieve_all_info()
4✔
861
        return SafeCliInfo(
4✔
862
            self.address,
4✔
863
            safe_info.nonce,
4✔
864
            safe_info.threshold,
4✔
865
            safe_info.owners,
4✔
866
            safe_info.master_copy,
4✔
867
            safe_info.modules,
4✔
868
            safe_info.fallback_handler,
4✔
869
            safe_info.guard,
4✔
870
            balance_ether,
4✔
871
            safe_info.version,
4✔
872
        )
873

874
    def get_threshold(self):
4✔
875
        print_formatted_text(self.safe.retrieve_threshold())
876

877
    def get_nonce(self):
4✔
878
        print_formatted_text(self.safe.retrieve_nonce())
4✔
879

880
    def get_owners(self):
4✔
881
        print_formatted_text(self.safe.retrieve_owners())
882

883
    def execute_safe_internal_transaction(self, data: bytes) -> bool:
4✔
884
        return self.prepare_and_execute_safe_transaction(self.address, 0, data)
4✔
885

886
    def prepare_safe_transaction(
4✔
887
        self,
888
        to: str,
4✔
889
        value: int,
4✔
890
        data: bytes,
4✔
891
        operation: SafeOperation = SafeOperation.CALL,
4✔
892
        safe_nonce: Optional[int] = None,
4✔
893
    ) -> SafeTx:
4✔
894
        safe_tx = self.safe.build_multisig_tx(
4✔
895
            to, value, data, operation=operation.value, safe_nonce=safe_nonce
4✔
896
        )
897
        self.sign_transaction(safe_tx)  # Raises exception if it cannot be signed
4✔
898
        return safe_tx
4✔
899

900
    def prepare_and_execute_safe_transaction(
4✔
901
        self,
902
        to: str,
4✔
903
        value: int,
4✔
904
        data: bytes,
4✔
905
        operation: SafeOperation = SafeOperation.CALL,
4✔
906
        safe_nonce: Optional[int] = None,
4✔
907
    ) -> bool:
4✔
908
        safe_tx = self.prepare_safe_transaction(
4✔
909
            to, value, data, operation, safe_nonce=safe_nonce
4✔
910
        )
911
        return self.execute_safe_transaction(safe_tx)
4✔
912

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

966
    # Batch_transactions multisend
967
    def batch_safe_txs(
4✔
968
        self, safe_nonce: int, safe_txs: Sequence[SafeTx]
4✔
969
    ) -> Optional[SafeTx]:
4✔
970
        """
971
        Submit signatures to the tx service. It's recommended to be on Safe v1.3.0 to prevent issues
972
        with `safeTxGas` and gas estimation.
973

974
        :return:
975
        """
976

977
        try:
4✔
978
            multisend = MultiSend(ethereum_client=self.ethereum_client)
4✔
979
        except ValueError:
980
            multisend = None
981
            print_formatted_text(
982
                HTML(
983
                    "<ansired>Multisend contract is not deployed on this network and it's required for "
984
                    "batching txs</ansired>"
985
                )
986
            )
987

988
        multisend_txs = []
4✔
989
        for safe_tx in safe_txs:
4✔
990
            # Check if call is already a Multisend call
991
            inner_txs = MultiSend.from_transaction_data(safe_tx.data)
4✔
992
            if inner_txs:
4✔
993
                multisend_txs.extend(inner_txs)
994
            else:
995
                multisend_txs.append(
4✔
996
                    MultiSendTx(
4✔
997
                        MultiSendOperation.CALL, safe_tx.to, safe_tx.value, safe_tx.data
4✔
998
                    )
999
                )
1000

1001
        if len(multisend_txs) == 1:
4✔
1002
            safe_tx.safe_tx_gas = 0
1003
            safe_tx.base_gas = 0
1004
            safe_tx.gas_price = 0
1005
            safe_tx.signatures = b""
1006
            safe_tx.safe_nonce = safe_nonce  # Resend single transaction
1007
        elif multisend:
4✔
1008
            safe_tx = SafeTx(
4✔
1009
                self.ethereum_client,
4✔
1010
                self.address,
4✔
1011
                multisend.address,
4✔
1012
                0,
4✔
1013
                multisend.build_tx_data(multisend_txs),
4✔
1014
                SafeOperation.DELEGATE_CALL.value,
4✔
1015
                0,
4✔
1016
                0,
4✔
1017
                0,
4✔
1018
                None,
4✔
1019
                None,
4✔
1020
                safe_nonce=safe_nonce,
4✔
1021
            )
1022
        else:
1023
            # Multisend not defined
1024
            return None
1025

1026
        safe_tx = self.sign_transaction(safe_tx)
4✔
1027
        if not safe_tx.signatures:
4✔
1028
            print_formatted_text(
1029
                HTML("<ansired>At least one owner must be loaded</ansired>")
1030
            )
1031
            return None
1032
        else:
1033
            return safe_tx
4✔
1034

1035
    def get_signers(self) -> Tuple[List[LocalAccount], List[HwWallet]]:
4✔
1036
        """
1037

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

1061
        if self.require_all_signatures and threshold > 0:
4✔
1062
            raise NotEnoughSignatures(threshold)
4✔
1063

1064
        return (eoa_signers, hw_wallet_signers)
4✔
1065

1066
    # TODO Set sender so we can save gas in that signature
1067
    def sign_transaction(self, safe_tx: SafeTx) -> SafeTx:
4✔
1068
        eoa_signers, hw_wallets_signers = self.get_signers()
4✔
1069
        for selected_account in eoa_signers:
4✔
1070
            safe_tx.sign(selected_account.key)
4✔
1071

1072
        # Sign with ledger
1073
        if len(hw_wallets_signers):
4✔
1074
            safe_tx = self.hw_wallet_manager.sign_eip712(safe_tx, hw_wallets_signers)
1075

1076
        return safe_tx
4✔
1077

1078
    @require_tx_service
4✔
1079
    def _require_tx_service_mode(self):
4✔
1080
        print_formatted_text(
1081
            HTML(
1082
                "<ansired>First enter tx-service mode using <b>tx-service</b> command</ansired>"
1083
            )
1084
        )
1085

1086
    def get_delegates(self):
4✔
1087
        return self._require_tx_service_mode()
1088

1089
    def add_delegate(self, delegate_address: str, label: str, signer_address: str):
4✔
1090
        return self._require_tx_service_mode()
1091

1092
    def remove_delegate(self, delegate_address: str, signer_address: str):
4✔
1093
        return self._require_tx_service_mode()
1094

1095
    def submit_signatures(self, safe_tx_hash: bytes) -> bool:
4✔
1096
        return self._require_tx_service_mode()
1097

1098
    def get_balances(self):
4✔
1099
        return self._require_tx_service_mode()
1100

1101
    def get_transaction_history(self):
4✔
1102
        return self._require_tx_service_mode()
1103

1104
    def batch_txs(self, safe_nonce: int, safe_tx_hashes: Sequence[bytes]) -> bool:
4✔
1105
        return self._require_tx_service_mode()
1106

1107
    def execute_tx(self, safe_tx_hash: Sequence[bytes]) -> bool:
4✔
1108
        return self._require_tx_service_mode()
1109

1110
    def get_permitted_signers(self) -> Set[ChecksumAddress]:
4✔
1111
        """
1112
        :return: Accounts that can sign a transaction
1113
        """
1114
        return set(self.safe_cli_info.owners)
4✔
1115

1116
    def drain(self, to: str):
4✔
1117
        # Getting all events related with ERC20 transfers
1118
        last = self.ethereum_client.get_block("latest")["number"]
4✔
1119
        token_addresses = get_erc_20_list(self.ethereum_client, self.address, 1, last)
4✔
1120
        safe_txs = []
4✔
1121
        for token_address in token_addresses:
4✔
1122
            balance = self.ethereum_client.erc20.get_balance(
4✔
1123
                self.address, token_address
4✔
1124
            )
1125
            if balance > 0:
4✔
1126
                transaction = (
4✔
1127
                    get_erc20_contract(self.ethereum_client.w3, token_address)
4✔
1128
                    .functions.transfer(to, balance)
4✔
1129
                    .build_transaction({"from": self.address, "gas": 0, "gasPrice": 0})
4✔
1130
                )
1131

1132
                safe_tx = self.prepare_safe_transaction(
4✔
1133
                    token_address,
4✔
1134
                    0,
4✔
1135
                    HexBytes(transaction["data"]),
4✔
1136
                    SafeOperation.CALL,
4✔
1137
                    safe_nonce=None,
4✔
1138
                )
1139
                safe_txs.append(safe_tx)
4✔
1140

1141
        # Getting ethereum balance
1142
        balance_eth = self.ethereum_client.get_balance(self.address)
4✔
1143
        if balance_eth:
4✔
1144
            safe_tx = self.prepare_safe_transaction(
4✔
1145
                to,
4✔
1146
                balance_eth,
4✔
1147
                b"",
4✔
1148
                SafeOperation.CALL,
4✔
1149
                safe_nonce=None,
4✔
1150
            )
1151
            safe_txs.append(safe_tx)
4✔
1152

1153
        if safe_txs:
4✔
1154
            multisend_tx = self.batch_safe_txs(self.get_nonce(), safe_txs)
4✔
1155
            if multisend_tx is not None:
4✔
1156
                if self.execute_safe_transaction(multisend_tx):
4✔
1157
                    print_formatted_text(
4✔
1158
                        HTML(
4✔
1159
                            "<ansigreen>Transaction to drain account correctly executed</ansigreen>"
4✔
1160
                        )
1161
                    )
1162
        else:
1163
            print_formatted_text(
1164
                HTML("<ansigreen>Safe account is currently empty</ansigreen>")
1165
            )
1166

1167
    def process_command(self, first_command: str, rest_command: List[str]) -> bool:
4✔
1168
        if first_command == "help":
1169
            print_formatted_text("I still cannot help you")
1170
        elif first_command == "refresh":
1171
            print_formatted_text("Reloading Safe information")
1172
            self.refresh_safe_cli_info()
1173

1174
        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