-
Notifications
You must be signed in to change notification settings - Fork 0
/
commands.py
278 lines (220 loc) · 9.14 KB
/
commands.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
import click
import web3
import json
from safe.utils import ADDRESS0, Operation, get_balance, code_exists, get_account_info_from_mnemonic
from safe import Safe
from ethereum.utils import ecsign
import codecs
from bip44.crypto import HDPrivateKey, HDKey
from eth_account.account import Account
INFURA_KEY = ''
ENVS = {
'mainnet': {
'name': 'Ethereum mainnet',
'rpc_endpoint_url': 'https://mainnet.infura.io/v3/{}'.format(INFURA_KEY),
'safe_relay_url': 'https://safe-relay.gnosis.pm',
'etherscan_url': 'https://etherscan.io'
},
'rinkeby': {
'name': 'Rinkeby testnet',
'rpc_endpoint_url': 'https://rinkeby.infura.io/v3/{}'.format(INFURA_KEY),
'safe_relay_url': 'https://safe-relay.rinkeby.gnosis.pm',
'etherscan_url': 'https://rinkeby.etherscan.io'
}
}
class ChecksumAddressParamType(click.ParamType):
name = 'checksum_address'
def convert(self, value, param, ctx):
if web3.Web3.isChecksumAddress(value):
return value
else:
self.fail(
'{} is not a valid checksum address'.format(value),
param,
ctx)
CHECKSUM_ADDRESS = ChecksumAddressParamType()
pass_safe = click.make_pass_decorator(Safe)
@click.group()
@click.argument('address', type=CHECKSUM_ADDRESS)
@click.option('-n', '--network', type=click.Choice(['mainnet', 'rinkeby']))
@click.version_option('0.0.3')
@click.pass_context
def cli(ctx, address, network):
"""Command line interface for the Gnosis Safe.
"""
if not network:
network = 'mainnet'
env = ENVS[network]
click.echo('\nYou are using: {}\n\n'.format(env['name']))
ctx.obj = Safe(network, address, env['rpc_endpoint_url'], env['safe_relay_url'])
@cli.command()
@pass_safe
def info(safe):
"""Shows info about a Safe.
This will show info such as balance, threshold and owners of a Safe.
"""
# Get general things.
click.echo('Safe at address: {}'.format(safe.address))
click.echo('{}/address/{}\n'.format(
ENVS[safe.network]['etherscan_url'], safe.address))
click.echo('ETH balance: {}\n'.format(safe.get_balance('ether')))
# Is there even code at the address?
if not code_exists(safe.w3, safe.address):
click.echo('No code found at provided address. Are you sure that you are on the right network?')
exit()
# Owners
owners = safe.get_owners()
threshold = safe.get_threshold()
click.echo('threshold/owners: {}/{}\n'.format(threshold, len(owners)))
for i, owner in enumerate(owners):
is_contract = code_exists(safe.w3, owner)
owner_eth_balance = get_balance(safe.w3, owner, 'ether')
click.echo('Owner {}: {} (code at address: {}, balance: {})'.format(
i,
owner,
is_contract,
owner_eth_balance))
@cli.command()
@pass_safe
def get_nonce(safe):
"""Show current nonce of the Safe.
This will show the current nonce of the Safe.
"""
for owner in safe.get_owners():
click.echo(owner)
@cli.command()
@pass_safe
def get_owners(safe):
"""Lists all owners of a Safe.
This will show the addresses of all owners.
"""
for owner in safe.get_owners():
click.echo(owner)
@cli.command()
@pass_safe
def get_threshold(safe):
"""Shows threshold of a Safe.
This will the currently set threshold of the Safe.
"""
click.echo(safe.get_threshold())
@cli.command()
@click.argument('to_address', type=CHECKSUM_ADDRESS)
@click.argument('ether_value', type=float)
@pass_safe
def transfer_ether(safe, to_address, ether_value):
"""Transfer ether to an account.
This will transfer the given amount of ether from the Safe to another account.
"""
safe_tx(safe, safe.transfer_ether_tx, to_address, ether_value)
@cli.command()
@click.argument('owner_address', type=CHECKSUM_ADDRESS)
@click.argument('threshold', type=int)
@pass_safe
def owner_add(safe, owner_address, threshold):
"""Add owner to a Safe.
This will add an owner to the Safe.
"""
safe_tx(safe, safe.owner_add_tx, owner_address, threshold)
@cli.command()
@click.argument('owner_address', type=CHECKSUM_ADDRESS)
@click.argument('threshold', type=int)
@pass_safe
def owner_remove(safe, owner_address, threshold):
"""Remove owner from a Safe.
This will remove an owner from the Safe.
"""
safe_tx(safe, safe.owner_remove_tx, owner_address, threshold)
@cli.command()
@click.argument('old_owner_address', type=CHECKSUM_ADDRESS)
@click.argument('new_owner_address', type=CHECKSUM_ADDRESS)
@pass_safe
def owner_swap(safe, old_owner_address, new_owner_address):
"""Swap owners of a Safe.
This will replace an existing owner of the Safe with a new one.
"""
safe_tx(safe, safe.owner_swap_tx, old_owner_address, new_owner_address)
@cli.command()
@click.argument('threshold', type=int)
@pass_safe
def owner_change_threshold(safe, threshold):
"""Change confirmation threshold of a Safe.
This will change the number of confirmations required to make a transaction with the Safe.
"""
safe_tx(safe, safe.owner_change_threshold_tx, threshold)
def safe_tx(safe, function, *params):
# build tx and get tx hash
transaction = safe.build_transaction(function, *params)
click.echo(transaction.transaction_semantics_text)
click.echo('\nGas price: {}\nsafeTxGas: {}\ndataGas: {}\nnonce: {}\n\n'.format(transaction.gas_price, transaction.safe_tx_gas, transaction.data_gas, transaction.nonce))
# check how many signatures are required
threshold = safe.get_threshold()
click.echo('Threshold: {}\n Please sign: {}\n\n'.format(threshold, transaction.hash.hex()))
signatures = []
for i in range(threshold):
signature = click.prompt('Signature {}/{}'.format(i+1, threshold)) # TODO validate format
signatures.append(json.loads(signature))
click.confirm('Good to go. Submit tx?')
click.echo('{}/tx/{}'.format(ENVS[safe.network]['etherscan_url'], safe.execute_transaction(transaction, signatures)['transactionHash']))
@cli.command()
@click.confirmation_option(help='Are you sure you want to delete the Safe?')
@pass_safe
def delete(safe):
"""Deletes a Safe.
This will throw away the current Safe.
"""
click.echo('You cannot delete a Safe. It will forever be on the blockchain! ¯\_(ツ)_/¯')
@cli.command()
@click.option('--multi', is_flag=True, help='Ask for multiple signatures until threshold. Comes in handy when signing multiple owners on one machine.')
@pass_safe
def sign(safe, multi):
"""Sign transation of a Safe.
This will sign a given transaction hash and return the signature.
"""
transaction_hash = click.prompt('Please enter transaction hash')
transaction_hash = codecs.decode(transaction_hash, 'hex_codec')
choice = click.prompt('What would you like to use for signing?\n(1) Private key\n(2) Account mnemonic\n(3) Safe mnemonic (Yields 2 signatures)\n', type=int)
loops = 1 if not multi else safe.get_threshold()
account_info = []
while loops > 0:
if choice == 1:
private_key = click.prompt('Please enter private key (Input hidden)', hide_input=True)
address = Account.privateKeyToAccount(private_key).address
account_info.append((private_key, address))
elif choice == 2:
mnemonic = click.prompt('Please enter account mnemonic (Input hidden)', hide_input=True)
account_info.append(get_account_info_from_mnemonic(mnemonic))
else:
mnemonic = click.prompt('Please enter Safe mnemonic (Input hidden)', hide_input=True)
account_info.append(get_account_info_from_mnemonic(mnemonic, index=0))
account_info.append(get_account_info_from_mnemonic(mnemonic, index=1))
for i, info in enumerate(account_info):
loops -= 1
private_key = info[0]
address = info[1]
v, r, s = ecsign(transaction_hash, codecs.decode(private_key, 'hex_codec'))
signature = {'v': v, 'r': r, 's': s}
click.echo('Signature {} ({}):\n\n{}'.format(i, address, json.dumps(signature)))
@cli.command()
@click.option('--no_check', is_flag=False, help='Do not check if the addresses are owners. Just print them.')
@pass_safe
def check_key(safe, no_check):
"""Check recovery key.
Print the 2 addresses the Safe recovery key stands for.
"""
mnemonic = click.prompt('Please enter Safe mnemonic (Input hidden)', hide_input=True)
addresses = [
safe.w3.toChecksumAddress(get_account_info_from_mnemonic(mnemonic, index=0)[1]),
safe.w3.toChecksumAddress(get_account_info_from_mnemonic(mnemonic, index=1)[1])
]
click.echo('Address #0: {}'.format(addresses[0]))
click.echo('Address #1: {}'.format(addresses[1]))
if not no_check:
owners = safe.get_owners()
if addresses[0] not in owners or addresses[1] not in addresses[1]:
click.echo('\nThis seems to be an invalid recovery key!')
click.echo('\nOwner addresses are:')
for owner in owners:
click.echo('\t{}'.format(owner))
if __name__ == '__main__':
# show_details()
pass