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

safe-global / safe-cli / 9464772673

11 Jun 2024 11:32AM UTC coverage: 87.16% (+6.5%) from 80.617%
9464772673

push

github

web-flow
Update project to use hatch (#404)

* Update project to use hatch

- Update pre-commit, as black previous version was having issues
- Use recommended structure: https://docs.pytest.org/en/stable/explanation/goodpractices.html
- Update CI
- Add run_tests.sh script

* Fix coverage

* Fix version

* Fix module export

* Fix linting

---------

Co-authored-by: Uxio Fuentefria <6909403+Uxio0@users.noreply.github.com>

722 of 835 branches covered (86.47%)

Branch coverage included in aggregate %.

3 of 3 new or added lines in 2 files covered. (100.0%)

2326 of 2662 relevant lines covered (87.38%)

3.49 hits per line

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

88.17
/src/safe_cli/safe_creator.py
1
#!/bin/env python3
2
import argparse
4✔
3
import secrets
4✔
4
import sys
4✔
5
from typing import List
4✔
6

7
from art import text2art
4✔
8
from eth_account import Account
4✔
9
from eth_account.signers.local import LocalAccount
4✔
10
from eth_typing import URI
4✔
11
from hexbytes import HexBytes
4✔
12
from prompt_toolkit import print_formatted_text
4✔
13

14
from gnosis.eth import EthereumClient, EthereumTxSent
4✔
15
from gnosis.eth.constants import NULL_ADDRESS
4✔
16
from gnosis.eth.contracts import get_safe_V1_4_1_contract
4✔
17
from gnosis.safe import ProxyFactory, Safe
4✔
18

19
from safe_cli.safe_addresses import (
4✔
20
    get_default_fallback_handler_address,
21
    get_proxy_factory_address,
22
    get_safe_contract_address,
23
    get_safe_l2_contract_address,
24
)
25
from safe_cli.utils import yes_or_no_question
4✔
26

27
from .argparse_validators import (
4✔
28
    check_ethereum_address,
29
    check_positive_integer,
30
    check_private_key,
31
)
32

33

34
def setup_argument_parser():
4✔
35
    parser = argparse.ArgumentParser()
4✔
36
    parser.add_argument("node_url", help="Ethereum node url")
4✔
37
    parser.add_argument(
4✔
38
        "private_key", help="Deployer private_key", type=check_private_key
39
    )
40
    parser.add_argument(
4✔
41
        "--threshold",
42
        help="Number of owners required to execute transactions on the created Safe. It must"
43
        "be greater than 0 and less or equal than the number of owners",
44
        type=check_positive_integer,
45
        default=1,
46
    )
47
    parser.add_argument(
4✔
48
        "--owners",
49
        help="Owners. By default it will be just the deployer",
50
        nargs="+",
51
        type=check_ethereum_address,
52
    )
53
    parser.add_argument(
4✔
54
        "--safe-contract",
55
        help="Use a custom Safe master copy",
56
        default=None,
57
        type=check_ethereum_address,
58
    )
59
    parser.add_argument(
4✔
60
        "--proxy-factory",
61
        help="Use a custom proxy factory",
62
        default=None,
63
        type=check_ethereum_address,
64
    )
65
    parser.add_argument(
4✔
66
        "--callback-handler",
67
        help="Use a custom fallback handler. It is not required for Safe Master Copies "
68
        "with version < 1.1.0",
69
        default=None,
70
        type=check_ethereum_address,
71
    )
72
    parser.add_argument(
4✔
73
        "--salt-nonce",
74
        help="Use a custom nonce for the deployment. Same nonce with same deployment configuration will "
75
        "lead to the same Safe address ",
76
        default=secrets.randbits(256),
77
        type=int,
78
    )
79

80
    parser.add_argument(
4✔
81
        "--without-events",
82
        help="Use non events deployment of the Safe instead of the regular one. Recommended for mainnet to save gas costs when using the Safe",
83
        default=False,
84
        action="store_true",
85
    )
86
    return parser
4✔
87

88

89
def main(*args, **kwargs) -> EthereumTxSent:
4✔
90
    parser = setup_argument_parser()
4✔
91
    print_formatted_text(text2art("Safe Creator"))  # Print fancy text
4✔
92
    args = parser.parse_args()
4✔
93
    node_url: URI = args.node_url
4✔
94
    account: LocalAccount = Account.from_key(args.private_key)
4✔
95
    owners: List[str] = args.owners if args.owners else [account.address]
4✔
96
    threshold: int = args.threshold
4✔
97
    salt_nonce: int = args.salt_nonce
4✔
98
    to = NULL_ADDRESS
4✔
99
    data = b""
4✔
100
    payment_token = NULL_ADDRESS
4✔
101
    payment = 0
4✔
102
    payment_receiver = NULL_ADDRESS
4✔
103

104
    if len(owners) < threshold:
4✔
105
        print_formatted_text(
×
106
            "Threshold cannot be bigger than the number of unique owners"
107
        )
108
        sys.exit(1)
×
109

110
    ethereum_client = EthereumClient(node_url)
4✔
111
    ethereum_network = ethereum_client.get_network()
4✔
112

113
    safe_contract_address = args.safe_contract or (
4✔
114
        get_safe_contract_address(ethereum_client)
115
        if args.without_events
116
        else get_safe_l2_contract_address(ethereum_client)
117
    )
118
    proxy_factory_address = args.proxy_factory or get_proxy_factory_address(
4✔
119
        ethereum_client
120
    )
121
    fallback_handler = args.callback_handler or get_default_fallback_handler_address(
4✔
122
        ethereum_client
123
    )
124

125
    if not ethereum_client.is_contract(safe_contract_address):
4✔
126
        print_formatted_text(
×
127
            f"Safe contract address {safe_contract_address} "
128
            f"does not exist on network {ethereum_network.name}"
129
        )
130
        sys.exit(1)
×
131
    elif not ethereum_client.is_contract(proxy_factory_address):
4✔
132
        print_formatted_text(
×
133
            f"Proxy contract address {proxy_factory_address} "
134
            f"does not exist on network {ethereum_network.name}"
135
        )
136
        sys.exit(1)
×
137
    elif fallback_handler != NULL_ADDRESS and not ethereum_client.is_contract(
4✔
138
        fallback_handler
139
    ):
140
        print_formatted_text(
×
141
            f"Fallback handler address {fallback_handler} "
142
            f"does not exist on network {ethereum_network.name}"
143
        )
144
        sys.exit(1)
×
145

146
    account_balance: int = ethereum_client.get_balance(account.address)
4✔
147
    if not account_balance:
4✔
148
        print_formatted_text(
×
149
            "Client does not have any funds. Let's try anyway in case it's a network without gas costs"
150
        )
151
    else:
152
        ether_account_balance = round(
4✔
153
            ethereum_client.w3.from_wei(account_balance, "ether"), 6
154
        )
155
        print_formatted_text(
4✔
156
            f"Network {ethereum_client.get_network().name} - Sender {account.address} - "
157
            f"Balance: {ether_account_balance}Ξ"
158
        )
159

160
    if not ethereum_client.w3.eth.get_code(
4✔
161
        safe_contract_address
162
    ) or not ethereum_client.w3.eth.get_code(proxy_factory_address):
163
        print_formatted_text("Network not supported")
×
164
        sys.exit(1)
×
165

166
    print_formatted_text(
4✔
167
        f"Creating new Safe with owners={owners} threshold={threshold} salt-nonce={salt_nonce}"
168
    )
169
    safe_version = Safe(safe_contract_address, ethereum_client).retrieve_version()
4✔
170
    print_formatted_text(
4✔
171
        f"Safe-master-copy={safe_contract_address} version={safe_version}\n"
172
        f"Fallback-handler={fallback_handler}\n"
173
        f"Proxy factory={proxy_factory_address}"
174
    )
175
    if yes_or_no_question("Do you want to continue?"):
4✔
176
        safe_contract = get_safe_V1_4_1_contract(
4✔
177
            ethereum_client.w3, safe_contract_address
178
        )
179
        safe_creation_tx_data = HexBytes(
4✔
180
            safe_contract.functions.setup(
181
                owners,
182
                threshold,
183
                to,
184
                data,
185
                fallback_handler,
186
                payment_token,
187
                payment,
188
                payment_receiver,
189
            ).build_transaction({"gas": 1, "gasPrice": 1})["data"]
190
        )
191

192
        proxy_factory = ProxyFactory(proxy_factory_address, ethereum_client)
4✔
193
        expected_safe_address = proxy_factory.calculate_proxy_address(
4✔
194
            safe_contract_address, safe_creation_tx_data, salt_nonce
195
        )
196
        if ethereum_client.is_contract(expected_safe_address):
4✔
197
            print_formatted_text(f"Safe on {expected_safe_address} is already deployed")
4✔
198
            sys.exit(1)
4✔
199

200
        if yes_or_no_question(
4✔
201
            f"Safe will be deployed on {expected_safe_address}, looks good?"
202
        ):
203
            ethereum_tx_sent = proxy_factory.deploy_proxy_contract_with_nonce(
4✔
204
                account, safe_contract_address, safe_creation_tx_data, salt_nonce
205
            )
206
            print_formatted_text(
4✔
207
                f"Sent tx with tx-hash={ethereum_tx_sent.tx_hash.hex()} "
208
                f"Safe={ethereum_tx_sent.contract_address} is being created"
209
            )
210
            print_formatted_text(f"Tx parameters={ethereum_tx_sent.tx}")
4✔
211
            return ethereum_tx_sent
4✔
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