hooks and workflow for 2of3 wallets
[electrum-nvc.git] / electrum
1 #!/usr/bin/env python
2 #
3 # Electrum - lightweight Bitcoin client
4 # Copyright (C) 2011 thomasv@gitorious
5 #
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
18
19 from decimal import Decimal
20 import json
21 import optparse
22 import os
23 import re
24 import ast
25 import sys
26 import time
27 import traceback
28
29 try:
30     import ecdsa  # todo: 'ecdsa' imported but unused
31 except ImportError:
32     sys.exit("Error: python-ecdsa does not seem to be installed. Try 'sudo pip install ecdsa'")
33
34 try:
35     import aes  # todo: 'aes' imported but unused
36 except ImportError:
37     sys.exit("Error: AES does not seem to be installed. Try 'sudo pip install slowaes'")
38
39
40 is_local = os.path.dirname(os.path.realpath(__file__)) == os.getcwd()
41 is_android = 'ANDROID_DATA' in os.environ
42
43 import __builtin__
44 __builtin__.use_local_modules = is_local or is_android
45
46 # load local module as electrum
47 if __builtin__.use_local_modules:
48     import imp
49     imp.load_module('electrum', *imp.find_module('lib'))
50     imp.load_module('electrum_gui', *imp.find_module('gui'))
51
52 from electrum import *  # todo: import * is generally frowned upon. should import just what is used
53
54
55 # get password routine
56 def prompt_password(prompt, confirm=True):
57     import getpass
58     if sys.stdin.isatty():
59         password = getpass.getpass(prompt)
60         if password and confirm:
61             password2 = getpass.getpass("Confirm: ")
62             if password != password2:
63                 sys.exit("Error: Passwords do not match.")
64     else:
65         password = raw_input(prompt)
66     if not password:
67         password = None
68     return password
69
70
71 def arg_parser():
72     usage = "%prog [options] command"
73     parser = optparse.OptionParser(prog=usage, add_help_option=False)
74     parser.add_option("-h", "--help", action="callback", callback=print_help_cb, help="show this help text")
75     parser.add_option("-g", "--gui", dest="gui", help="User interface: qt, lite, gtk, text or stdio")
76     parser.add_option("-w", "--wallet", dest="wallet_path", help="wallet path (default: electrum.dat)")
77     parser.add_option("-o", "--offline", action="store_true", dest="offline", default=False, help="remain offline")
78     parser.add_option("-C", "--concealed", action="store_true", dest="concealed", default=False, help="don't echo seed to console when restoring")
79     parser.add_option("-a", "--all", action="store_true", dest="show_all", default=False, help="show all addresses")
80     parser.add_option("-l", "--labels", action="store_true", dest="show_labels", default=False, help="show the labels of listed addresses")
81     parser.add_option("-f", "--fee", dest="tx_fee", default=None, help="set tx fee")
82     parser.add_option("-F", "--fromaddr", dest="from_addr", default=None, help="set source address for payto/mktx. if it isn't in the wallet, it will ask for the private key unless supplied in the format public_key:private_key. It's not saved in the wallet.")
83     parser.add_option("-c", "--changeaddr", dest="change_addr", default=None, help="set the change address for payto/mktx. default is a spare address, or the source address if it's not in the wallet")
84     parser.add_option("-s", "--server", dest="server", default=None, help="set server host:port:protocol, where protocol is either t (tcp), h (http), s (tcp+ssl), or g (https)")
85     parser.add_option("-p", "--proxy", dest="proxy", default=None, help="set proxy [type:]host[:port], where type is socks4,socks5 or http")
86     parser.add_option("-v", "--verbose", action="store_true", dest="verbose", default=False, help="show debugging information")
87     parser.add_option("-P", "--portable", action="store_true", dest="portable", default=False, help="portable wallet")
88     parser.add_option("-L", "--lang", dest="language", default=None, help="defaut language used in GUI")
89     parser.add_option("-u", "--usb", dest="bitkey", action="store_true", help="Turn on support for hardware wallets (EXPERIMENTAL)")
90     parser.add_option("-G", "--gap", dest="gap_limit", default=None, help="gap limit")
91     parser.add_option("-W", "--password", dest="password", default=None, help="set password for usage with commands (currently only implemented for create command, do not use it for longrunning gui session since the password is visible in /proc)")
92     parser.add_option("-1", "--oneserver", action="store_true", dest="oneserver", default=False, help="connect to one server only")
93     parser.add_option("--bip32", action="store_true", dest="bip32", default=False, help="bip32 (not final)")
94     parser.add_option("--2of3", action="store_true", dest="2of3", default=False, help="create 2of3 wallet")
95     parser.add_option("--mpk", dest="mpk", default=False, help="restore from master public key")
96     return parser
97
98
99 def print_help(parser):
100     parser.print_help()
101     print_msg("Type 'electrum help <command>' to see the help for a specific command")
102     print_msg("Type 'electrum --help' to see the list of options")
103     run_command(known_commands['help'])
104     sys.exit(1)
105
106
107 def print_help_cb(self, opt, value, parser):
108     print_help(parser)
109
110
111 def run_command(cmd, password=None, args=[]):
112     import socket
113     if cmd.requires_network and not options.offline:
114         network = NetworkProxy(config)
115         if not network.start(start_daemon= (True if cmd.name!='daemon' else False)):
116             print "Daemon not running"
117             sys.exit(1)
118
119
120
121         if wallet:
122             wallet.start_threads(network)
123             wallet.update()
124     else:
125         network = None
126
127     cmd_runner = Commands(wallet, network)
128     func = getattr(cmd_runner, cmd.name)
129     cmd_runner.password = password
130     try:
131         result = func(*args[1:])
132     except Exception:
133         traceback.print_exc(file=sys.stdout)
134         sys.exit(1)
135
136
137     if cmd.requires_network and not options.offline:
138         if wallet:
139             wallet.stop_threads()
140
141
142     if type(result) == str:
143         util.print_msg(result)
144     elif result is not None:
145         util.print_json(result)
146
147
148
149
150
151
152 if __name__ == '__main__':
153
154     parser = arg_parser()
155     options, args = parser.parse_args()
156     if options.portable and options.wallet_path is None:
157         options.electrum_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'electrum_data')
158
159     # config is an object passed to the various constructors (wallet, interface, gui)
160     if is_android:
161         config_options = {
162             'portable': True,
163             'verbose': True,
164             'gui': 'android',
165             'auto_cycle': True,
166         }
167     else:
168         config_options = eval(str(options))
169         for k, v in config_options.items():
170             if v is None:
171                 config_options.pop(k)
172
173     set_verbosity(config_options.get('verbose'))
174
175     config = SimpleConfig(config_options)
176
177     if len(args) == 0:
178         url = None
179         cmd = 'gui'
180     elif len(args) == 1 and re.match('^bitcoin:', args[0]):
181         url = args[0]
182         cmd = 'gui'
183     else:
184         cmd = args[0]
185
186     if cmd == 'gui':
187         gui_name = config.get('gui', 'classic')
188         if gui_name in ['lite', 'classic']:
189             gui_name = 'qt'
190         try:
191             gui = __import__('electrum_gui.' + gui_name, fromlist=['electrum_gui'])
192         except ImportError:
193             traceback.print_exc(file=sys.stdout)
194             sys.exit()
195             #sys.exit("Error: Unknown GUI: " + gui_name )
196
197         # network interface
198         if not options.offline:
199             network = Network(config)
200             network.start()
201         else:
202             network = None
203
204         gui = gui.ElectrumGui(config, network)
205         gui.main(url)
206
207         if network:
208             network.stop()
209
210         # we use daemon threads, their termination is enforced.
211         # this sleep command gives them time to terminate cleanly.
212         time.sleep(0.1)
213         sys.exit(0)
214
215     if cmd not in known_commands:
216         cmd = 'help'
217
218     cmd = known_commands[cmd]
219
220     # instanciate wallet for command-line
221     storage = WalletStorage(config)
222
223
224     if cmd.name in ['create', 'restore']:
225         if storage.file_exists:
226             sys.exit("Error: Remove the existing wallet first!")
227         if options.password is not None:
228             password = options.password
229         elif cmd.name == 'restore' and options.mpk:
230             password = None
231         else:
232             password = prompt_password("Password (hit return if you do not wish to encrypt your wallet):")
233
234         # if config.server is set, the user either passed the server on command line
235         # or chose it previously already. if he didn't pass a server on the command line,
236         # we just pick up a random one.
237         if not config.get('server'):
238             config.set_key('server', pick_random_server())
239
240         #fee = options.tx_fee if options.tx_fee else raw_input("fee (default:%s):" % (str(Decimal(wallet.fee)/100000000)))
241         #gap = options.gap_limit if options.gap_limit else raw_input("gap limit (default 5):")
242         #if fee:
243         #    wallet.set_fee(float(fee)*100000000)
244         #if gap:
245         #    wallet.change_gap_limit(int(gap))
246
247         if cmd.name == 'restore':
248             if options.mpk:
249                 wallet = Wallet.from_mpk(options.mpk, storage)
250             else:
251                 import getpass
252                 seed = getpass.getpass(prompt="seed:", stream=None) if options.concealed else raw_input("seed:")
253                 wallet = Wallet.from_seed(str(seed),storage)
254                 if not wallet:
255                     sys.exit("Error: Invalid seed")
256                 wallet.save_seed(password)
257
258             if not options.offline:
259                 network = Network(config)
260                 network.start()
261                 wallet.start_threads(network)
262                 print_msg("Recovering wallet...")
263                 wallet.restore(lambda x: x)
264                 if wallet.is_found():
265                     print_msg("Recovery successful")
266                 else:
267                     print_msg("Warning: Found no history for this wallet")
268             else:
269                 wallet.synchronize()
270                 print_msg("Warning: This wallet was restored offline. It may contain more addresses than displayed.")
271
272         else:
273             if not config.get('2of3'):
274                 wallet = Wallet(storage)
275                 wallet.init_seed(None)
276                 wallet.save_seed(password)
277                 wallet.synchronize()
278                 print_msg("Your wallet generation seed is:\n\"%s\"" % wallet.get_mnemonic(password))
279                 print_msg("Please keep it in a safe place; if you lose it, you will not be able to restore your wallet.")
280             else:
281                 wallet = Wallet_2of3(storage)
282                 cold_seed = wallet.init_cold_seed()
283                 print_msg("Your cold seed is:\n\"%s\"" % cold_seed)
284                 print_msg("Please store it on paper. ")
285                 print_msg("Open this file on your online computer to complete your wallet creation.")
286
287
288         print_msg("Wallet saved in '%s'" % wallet.storage.path)
289
290         # terminate
291         sys.exit(0)
292
293
294     if cmd.name not in ['create', 'restore'] and cmd.requires_wallet and not storage.file_exists:
295         print_msg("Error: Wallet file not found.")
296         print_msg("Type 'electrum create' to create a new wallet, or provide a path to a wallet with the -w option")
297         sys.exit(0)
298
299
300     if cmd.requires_wallet:
301         wallet = Wallet(storage)
302     else:
303         wallet = None
304
305
306     # important warning
307     if cmd.name in ['dumpprivkey', 'dumpprivkeys']:
308         print_msg("WARNING: ALL your private keys are secret.")
309         print_msg("Exposing a single private key can compromise your entire wallet!")
310         print_msg("In particular, DO NOT use 'redeem private key' services proposed by third parties.")
311
312     # commands needing password
313     if cmd.requires_password:
314         if wallet.seed == '':
315             seed = ''
316             password = None
317         elif wallet.use_encryption:
318             password = prompt_password('Password:', False)
319             if not password:
320                 print_msg("Error: Password required")
321                 sys.exit(1)
322             # check password
323             try:
324                 seed = wallet.get_seed(password)
325             except Exception:
326                 print_msg("Error: This password does not decode this wallet.")
327                 sys.exit(1)
328         else:
329             password = None
330             seed = wallet.get_seed(None)
331     else:
332         password = None
333
334     # add missing arguments, do type conversions
335     if cmd.name == 'importprivkey':
336         # See if they specificed a key on the cmd line, if not prompt
337         if len(args) == 1:
338             args[1] = prompt_password('Enter PrivateKey (will not echo):', False)
339
340     elif cmd.name == 'signrawtransaction':
341         args = [cmd, args[1], json.loads(args[2]) if len(args) > 2 else [], json.loads(args[3]) if len(args) > 3 else []]
342
343     elif cmd.name == 'createmultisig':
344         args = [cmd, int(args[1]), json.loads(args[2])]
345
346     elif cmd.name == 'createrawtransaction':
347         args = [cmd, json.loads(args[1]), json.loads(args[2])]
348
349     elif cmd.name == 'listaddresses':
350         args = [cmd, options.show_all, options.show_labels]
351
352     elif cmd.name in ['payto', 'mktx']:
353         domain = [options.from_addr] if options.from_addr else None
354         args = ['mktx', args[1], Decimal(args[2]), Decimal(options.tx_fee) if options.tx_fee else None, options.change_addr, domain]
355
356     elif cmd.name in ['paytomany', 'mksendmanytx']:
357         domain = [options.from_addr] if options.from_addr else None
358         outputs = []
359         for i in range(1, len(args), 2):
360             if len(args) < i+2:
361                 print_msg("Error: Mismatched arguments.")
362                 sys.exit(1)
363             outputs.append((args[i], Decimal(args[i+1])))
364         args = ['mksendmanytx', outputs, Decimal(options.tx_fee) if options.tx_fee else None, options.change_addr, domain]
365
366     elif cmd.name == 'help':
367         if len(args) < 2:
368             print_help(parser)
369
370     # check the number of arguments
371     if len(args) - 1 < cmd.min_args:
372         print_msg("Not enough arguments")
373         print_msg("Syntax:", cmd.syntax)
374         sys.exit(1)
375
376     if cmd.max_args >= 0 and len(args) - 1 > cmd.max_args:
377         print_msg("too many arguments", args)
378         print_msg("Syntax:", cmd.syntax)
379         sys.exit(1)
380
381     if cmd.max_args < 0:
382         if len(args) > cmd.min_args + 1:
383             message = ' '.join(args[cmd.min_args:])
384             print_msg("Warning: Final argument was reconstructed from several arguments:", repr(message))
385             args = args[0:cmd.min_args] + [message]
386
387
388
389     # run the command
390     if cmd.name == 'deseed':
391         if not wallet.seed:
392             print_msg("Error: This wallet has no seed")
393         else:
394             ns = wallet.storage.path + '.seedless'
395             print_msg("Warning: you are going to create a seedless wallet'\nIt will be saved in '%s'" % ns)
396             if raw_input("Are you sure you want to continue? (y/n) ") in ['y', 'Y', 'yes']:
397                 wallet.storage.path = ns
398                 wallet.seed = ''
399                 wallet.storage.put('seed', '', True)
400                 wallet.use_encryption = False
401                 wallet.storage.put('use_encryption', wallet.use_encryption, True)
402                 for k in wallet.imported_keys.keys():
403                     wallet.imported_keys[k] = ''
404                 wallet.storage.put('imported_keys', wallet.imported_keys, True)
405                 print_msg("Done.")
406             else:
407                 print_msg("Action canceled.")
408
409     elif cmd.name == 'getconfig':
410         key = args[1]
411         out = config.get(key)
412         print_msg(out)
413
414     elif cmd.name == 'setconfig':
415         key, value = args[1:3]
416         try:
417             value = ast.literal_eval(value)
418         except:
419             pass
420         config.set_key(key, value, True)
421         print_msg(True)
422
423     elif cmd.name == 'password':
424         new_password = prompt_password('New password:')
425         wallet.update_password(password, new_password)
426
427     else:
428         run_command(cmd, password, args)
429
430
431     time.sleep(0.1)
432     sys.exit(0)