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

safe-global / safe-cli / 13029017962

29 Jan 2025 10:10AM UTC coverage: 88.518% (-0.09%) from 88.612%
13029017962

Pull #493

github

web-flow
Merge 16eded0f4 into 255e7d0e3
Pull Request #493: Add parameter to generate vanity addresses

223 of 265 branches covered (84.15%)

Branch coverage included in aggregate %.

22 of 26 new or added lines in 1 file covered. (84.62%)

10 existing lines in 1 file now uncovered.

2876 of 3236 relevant lines covered (88.88%)

2.67 hits per line

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

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

7
from art import text2art
3✔
8
from eth_account import Account
3✔
9
from eth_account.signers.local import LocalAccount
3✔
10
from eth_typing import URI
3✔
11
from hexbytes import HexBytes
3✔
12
from prompt_toolkit import print_formatted_text
3✔
13
from safe_eth.eth import EthereumClient, EthereumTxSent
3✔
14
from safe_eth.eth.constants import NULL_ADDRESS
3✔
15
from safe_eth.eth.contracts import get_safe_V1_4_1_contract
3✔
16
from safe_eth.safe import ProxyFactory, Safe
3✔
17

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

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

33

34
def get_epilog_msg():
3✔
35
    return """
3✔
36
        Example:
37
            safe-creator https://sepolia.drpc.org 0000000000000000000000000000000000000000000000000000000000000000
38
    """
39

40

41
def setup_argument_parser():
3✔
42
    parser = argparse.ArgumentParser(description=get_epilog_msg())
3✔
43
    # parser = argparse.ArgumentParser(usage=get_usage_msg())
44
    parser.add_argument(
3✔
45
        "-v",
46
        "--version",
47
        action="version",
48
        version=f"Safe Creator v{VERSION}",
49
        help="Show program's version number and exit.",
50
    )
51
    parser.add_argument("node_url", help="Ethereum node url")
3✔
52
    parser.add_argument(
3✔
53
        "private_key", help="Deployer private_key", type=check_private_key
54
    )
55
    parser.add_argument(
56
        "--no-confirm",
57
        help="Bypass any and all “Yes or no?” messages",
58
        default=False,
59
        action="store_true",
60
    )
61
    parser.add_argument(
3✔
62
        "--threshold",
63
        help="Number of owners required to execute transactions on the created Safe. It must"
64
        "be greater than 0 and less or equal than the number of owners",
65
        type=check_positive_integer,
66
        default=1,
67
    )
68
    parser.add_argument(
3✔
69
        "--owners",
70
        help="Owners. By default it will be just the deployer",
71
        nargs="+",
72
        type=check_ethereum_address,
73
    )
74
    parser.add_argument(
3✔
75
        "--safe-contract",
76
        help="Use a custom Safe master copy",
77
        default=None,
78
        type=check_ethereum_address,
79
    )
80
    parser.add_argument(
3✔
81
        "--proxy-factory",
82
        help="Use a custom proxy factory",
83
        default=None,
84
        type=check_ethereum_address,
85
    )
86
    parser.add_argument(
3✔
87
        "--callback-handler",
88
        help="Use a custom fallback handler. It is not required for Safe Master Copies "
89
        "with version < 1.1.0",
90
        default=None,
91
        type=check_ethereum_address,
92
    )
93
    parser.add_argument(
3✔
94
        "--salt-nonce",
95
        help="Use a custom nonce for the deployment. Same nonce with same deployment configuration will "
96
        "lead to the same Safe address ",
97
        default=secrets.randbits(256),
98
        type=int,
99
    )
100
    parser.add_argument(
3✔
101
        "--without-events",
102
        help="Use non events deployment of the Safe instead of the regular one. Recommended for mainnet to save gas costs when using the Safe",
103
        default=False,
104
        action="store_true",
105
    )
106
    parser.add_argument(
3✔
107
        "--generate-vanity-addresses",
108
        help="Don't deploy the Safe, only generate addresses",
109
        default=False,
110
        action="store_true",
111
    )
112

113
    return parser
3✔
114

115

116
def main(*args, **kwargs) -> EthereumTxSent | None:
3✔
117
    print_formatted_text(text2art("Safe Creator"))  # Print fancy text
3✔
118

119
    parser = setup_argument_parser()
3✔
120
    args = parser.parse_args()
3✔
121
    node_url: URI = args.node_url
3✔
122
    account: LocalAccount = Account.from_key(args.private_key)
3✔
123
    no_confirm: bool = args.no_confirm
3✔
124
    owners: List[str] = args.owners if args.owners else [account.address]
3✔
125
    threshold: int = args.threshold
3✔
126
    salt_nonce: int = args.salt_nonce
3✔
127
    without_events: bool = args.without_events
3✔
128
    generate_vanity_addresses: bool = args.generate_vanity_addresses
3✔
129
    to = NULL_ADDRESS
3✔
130
    data = b""
3✔
131
    payment_token = NULL_ADDRESS
3✔
132
    payment = 0
3✔
133
    payment_receiver = NULL_ADDRESS
3✔
134

135
    if len(owners) < threshold:
3✔
UNCOV
136
        print_formatted_text(
×
137
            "Threshold cannot be bigger than the number of unique owners"
138
        )
UNCOV
139
        sys.exit(1)
×
140

141
    def yes_or_no(prompt: str) -> bool:
3✔
142
        if no_confirm:
3✔
NEW
143
            return True
×
144
        return yes_or_no_question(prompt)
3✔
145

146
    ethereum_client = EthereumClient(node_url)
3✔
147
    ethereum_network = ethereum_client.get_network()
3✔
148

149
    safe_contract_address = args.safe_contract or (
3✔
150
        get_safe_contract_address(ethereum_client)
151
        if without_events
152
        else get_safe_l2_contract_address(ethereum_client)
153
    )
154
    proxy_factory_address = args.proxy_factory or get_proxy_factory_address(
3✔
155
        ethereum_client
156
    )
157
    fallback_handler = args.callback_handler or get_default_fallback_handler_address(
3✔
158
        ethereum_client
159
    )
160

161
    if not ethereum_client.is_contract(safe_contract_address):
3✔
UNCOV
162
        print_formatted_text(
×
163
            f"Safe contract address {safe_contract_address} "
164
            f"does not exist on network {ethereum_network.name}"
165
        )
UNCOV
166
        sys.exit(1)
×
167
    elif not ethereum_client.is_contract(proxy_factory_address):
3✔
UNCOV
168
        print_formatted_text(
×
169
            f"Proxy contract address {proxy_factory_address} "
170
            f"does not exist on network {ethereum_network.name}"
171
        )
UNCOV
172
        sys.exit(1)
×
173
    elif fallback_handler != NULL_ADDRESS and not ethereum_client.is_contract(
3✔
174
        fallback_handler
175
    ):
UNCOV
176
        print_formatted_text(
×
177
            f"Fallback handler address {fallback_handler} "
178
            f"does not exist on network {ethereum_network.name}"
179
        )
UNCOV
180
        sys.exit(1)
×
181

182
    account_balance: int = ethereum_client.get_balance(account.address)
3✔
183
    if not account_balance:
3✔
UNCOV
184
        print_formatted_text(
×
185
            "Client does not have any funds. Let's try anyway in case it's a network without gas costs"
186
        )
187
    else:
188
        ether_account_balance = round(
3✔
189
            ethereum_client.w3.from_wei(account_balance, "ether"), 6
190
        )
191
        print_formatted_text(
3✔
192
            f"Network {ethereum_client.get_network().name} - Sender {account.address} - "
193
            f"Balance: {ether_account_balance}Ξ"
194
        )
195

196
    if not ethereum_client.w3.eth.get_code(
3✔
197
        safe_contract_address
198
    ) or not ethereum_client.w3.eth.get_code(proxy_factory_address):
199
        print_formatted_text("Network not supported")
×
UNCOV
200
        sys.exit(1)
×
201

202
    print_formatted_text(
3✔
203
        f"Creating new Safe with owners={owners} threshold={threshold} salt-nonce={salt_nonce}"
204
    )
205
    safe_version = Safe(safe_contract_address, ethereum_client).retrieve_version()
3✔
206
    print_formatted_text(
3✔
207
        f"Safe-master-copy={safe_contract_address} version={safe_version}\n"
208
        f"Fallback-handler={fallback_handler}\n"
209
        f"Proxy factory={proxy_factory_address}"
210
    )
211
    if yes_or_no("Do you want to continue?"):
3✔
212
        safe_contract = get_safe_V1_4_1_contract(
3✔
213
            ethereum_client.w3, safe_contract_address
214
        )
215
        safe_creation_tx_data = HexBytes(
3✔
216
            safe_contract.functions.setup(
217
                owners,
218
                threshold,
219
                to,
220
                data,
221
                fallback_handler,
222
                payment_token,
223
                payment,
224
                payment_receiver,
225
            ).build_transaction({"gas": 1, "gasPrice": 1})["data"]
226
        )
227

228
        proxy_factory = ProxyFactory(proxy_factory_address, ethereum_client)
3✔
229
        if generate_vanity_addresses:
3✔
NEW
230
            for vanity_salt_nonce in range(2**256):
×
NEW
231
                expected_safe_address = proxy_factory.calculate_proxy_address(
×
232
                    safe_contract_address, safe_creation_tx_data, vanity_salt_nonce
233
                )
NEW
234
                print_formatted_text(f"{expected_safe_address} {vanity_salt_nonce}")
×
235
        else:
236
            expected_safe_address = proxy_factory.calculate_proxy_address(
3✔
237
                safe_contract_address, safe_creation_tx_data, salt_nonce
238
            )
239
            if ethereum_client.is_contract(expected_safe_address):
3✔
240
                print_formatted_text(
3✔
241
                    f"Safe on {expected_safe_address} is already deployed"
242
                )
243
                sys.exit(1)
3✔
244

245
            if yes_or_no(
3✔
246
                f"Safe will be deployed on {expected_safe_address}, looks good?"
247
            ):
248
                ethereum_tx_sent = proxy_factory.deploy_proxy_contract_with_nonce(
3✔
249
                    account, safe_contract_address, safe_creation_tx_data, salt_nonce
250
                )
251
                print_formatted_text(
3✔
252
                    f"Sent tx with tx-hash={ethereum_tx_sent.tx_hash.hex()} "
253
                    f"Safe={ethereum_tx_sent.contract_address} is being created"
254
                )
255
                print_formatted_text(f"Tx parameters={ethereum_tx_sent.tx}")
3✔
256
                return ethereum_tx_sent
3✔
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