eval arg passed to setconfig, to avoid storing boolean as string
[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("--mpk", dest="mpk", default=False, help="restore from master public key")
95     return parser
96
97
98 def print_help(parser):
99     parser.print_help()
100     print_msg("Type 'electrum help <command>' to see the help for a specific command")
101     print_msg("Type 'electrum --help' to see the list of options")
102     run_command(known_commands['help'])
103     sys.exit(1)
104
105
106 def print_help_cb(self, opt, value, parser):
107     print_help(parser)
108
109
110 def run_command(cmd, password=None, args=[]):
111     import xmlrpclib, socket
112     cmd_runner = Commands(wallet, network)
113     func = getattr(cmd_runner, cmd.name)
114     cmd_runner.password = password
115
116     if cmd.requires_network and not options.offline:
117         cmd_runner.network = xmlrpclib.ServerProxy('http://localhost:8000')
118
119         while True:
120             try:
121                 if cmd_runner.network.ping() == 'pong':
122                     break
123             except socket.error:
124                 if cmd.name != 'daemon':
125                     start_daemon()
126                 else:
127                     print "Daemon not running"
128                     sys.exit(1)
129
130         if wallet:
131             wallet.start_threads(cmd_runner.network)
132             wallet.update()
133     else:
134         cmd_runner.network = None
135
136     try:
137         result = func(*args[1:])
138     except Exception:
139         print "ecxeption"
140         traceback.print_exc(file=sys.stdout)
141         sys.exit(1)
142
143
144     if cmd.requires_network and not options.offline:
145         if wallet:
146             wallet.stop_threads()
147
148
149     if type(result) == str:
150         util.print_msg(result)
151     elif result is not None:
152         util.print_json(result)
153
154
155
156 def start_server():
157     network = Network(config)
158     if not network.start(wait=True):
159         print_msg("Not connected, aborting.")
160         sys.exit(1)
161     print_msg("Network daemon connected to " + network.interface.connection_msg)
162     from SimpleXMLRPCServer import SimpleXMLRPCServer
163     server = SimpleXMLRPCServer(('localhost',8000), allow_none=True, logRequests=False)
164     server.network = network
165     server.register_function(lambda: 'pong', 'ping')
166     server.register_function(network.synchronous_get, 'synchronous_get')
167     server.register_function(network.get_servers, 'get_servers')
168     server.register_function(network.main_server, 'main_server')
169     server.register_function(network.send, 'send')
170     server.register_function(network.subscribe, 'subscribe')
171     server.register_function(network.is_connected, 'is_connected')
172     server.register_function(network.is_up_to_date, 'is_up_to_date')
173     server.register_function(lambda: setattr(server,'running', False), 'stop')
174     return server
175
176 def start_daemon():
177     pid = os.fork()
178     if (pid == 0): # The first child.
179         os.chdir("/")
180         os.setsid()
181         os.umask(0)
182         pid2 = os.fork()
183         if (pid2 == 0):  # Second child
184             server = start_server()
185             server.running = True
186             timeout = 60
187             t0 = time.time()
188             server.socket.settimeout(timeout)
189             while server.running:
190                 server.handle_request()
191                 t = time.time()
192                 if t - t0 > 0.9*timeout:
193                     break
194                 if not server.network.is_connected():
195                     break
196                 t0 = t
197         sys.exit(0)
198
199     time.sleep(2)
200
201
202 if __name__ == '__main__':
203
204     parser = arg_parser()
205     options, args = parser.parse_args()
206     if options.portable and options.wallet_path is None:
207         options.electrum_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'electrum_data')
208
209     # config is an object passed to the various constructors (wallet, interface, gui)
210     if is_android:
211         config_options = {
212             'portable': True,
213             'verbose': True,
214             'gui': 'android',
215             'auto_cycle': True,
216         }
217     else:
218         config_options = eval(str(options))
219         for k, v in config_options.items():
220             if v is None:
221                 config_options.pop(k)
222
223     set_verbosity(config_options.get('verbose'))
224
225     config = SimpleConfig(config_options)
226
227     if len(args) == 0:
228         url = None
229         cmd = 'gui'
230     elif len(args) == 1 and re.match('^bitcoin:', args[0]):
231         url = args[0]
232         cmd = 'gui'
233     else:
234         cmd = args[0]
235
236     if cmd == 'gui':
237         gui_name = config.get('gui', 'classic')
238         if gui_name in ['lite', 'classic']:
239             gui_name = 'qt'
240         try:
241             gui = __import__('electrum_gui.' + gui_name, fromlist=['electrum_gui'])
242         except ImportError:
243             traceback.print_exc(file=sys.stdout)
244             sys.exit()
245             #sys.exit("Error: Unknown GUI: " + gui_name )
246
247         # network interface
248         if not options.offline:
249             network = Network(config)
250             network.start()
251         else:
252             network = None
253
254         gui = gui.ElectrumGui(config, network)
255         gui.main(url)
256
257         if network:
258             network.stop()
259
260         # we use daemon threads, their termination is enforced.
261         # this sleep command gives them time to terminate cleanly.
262         time.sleep(0.1)
263         sys.exit(0)
264
265     if cmd not in known_commands:
266         cmd = 'help'
267
268     cmd = known_commands[cmd]
269
270     # instanciate wallet for command-line
271     storage = WalletStorage(config)
272
273
274     if cmd.name in ['create', 'restore']:
275         if storage.file_exists:
276             sys.exit("Error: Remove the existing wallet first!")
277         if options.password is not None:
278             password = options.password
279         elif cmd.name == 'restore' and options.mpk:
280             password = None
281         else:
282             password = prompt_password("Password (hit return if you do not wish to encrypt your wallet):")
283
284         # if config.server is set, the user either passed the server on command line
285         # or chose it previously already. if he didn't pass a server on the command line,
286         # we just pick up a random one.
287         if not config.get('server'):
288             config.set_key('server', pick_random_server())
289
290         #fee = options.tx_fee if options.tx_fee else raw_input("fee (default:%s):" % (str(Decimal(wallet.fee)/100000000)))
291         #gap = options.gap_limit if options.gap_limit else raw_input("gap limit (default 5):")
292         #if fee:
293         #    wallet.set_fee(float(fee)*100000000)
294         #if gap:
295         #    wallet.change_gap_limit(int(gap))
296
297         if cmd.name == 'restore':
298             if options.mpk:
299                 wallet = Wallet.from_mpk(options.mpk, storage)
300             else:
301                 import getpass
302                 seed = getpass.getpass(prompt="seed:", stream=None) if options.concealed else raw_input("seed:")
303                 wallet = Wallet.from_seed(str(seed),storage)
304                 if not wallet:
305                     sys.exit("Error: Invalid seed")
306                 wallet.save_seed(password)
307
308             if not options.offline:
309                 network = Network(config)
310                 network.start()
311                 wallet.start_threads(network)
312                 print_msg("Recovering wallet...")
313                 wallet.restore(lambda x: x)
314                 if wallet.is_found():
315                     print_msg("Recovery successful")
316                 else:
317                     print_msg("Warning: Found no history for this wallet")
318             else:
319                 wallet.synchronize()
320                 print_msg("Warning: This wallet was restored offline. It may contain more addresses than displayed.")
321
322         else:
323             wallet = Wallet(storage)
324             wallet.init_seed(None)
325             wallet.save_seed(password)
326             wallet.synchronize()
327             print_msg("Your wallet generation seed is:\n\"%s\"" % wallet.get_mnemonic(password))
328             print_msg("Please keep it in a safe place; if you lose it, you will not be able to restore your wallet.")
329
330         print_msg("Wallet saved in '%s'" % wallet.storage.path)
331
332         # terminate
333         sys.exit(0)
334
335
336     if cmd.name not in ['create', 'restore'] and cmd.requires_wallet and not storage.file_exists:
337         print_msg("Error: Wallet file not found.")
338         print_msg("Type 'electrum create' to create a new wallet, or provide a path to a wallet with the -w option")
339         sys.exit(0)
340
341
342     if cmd.requires_wallet:
343         wallet = Wallet(storage)
344     else:
345         wallet = None
346
347
348     # important warning
349     if cmd.name in ['dumpprivkey', 'dumpprivkeys']:
350         print_msg("WARNING: ALL your private keys are secret.")
351         print_msg("Exposing a single private key can compromise your entire wallet!")
352         print_msg("In particular, DO NOT use 'redeem private key' services proposed by third parties.")
353
354     # commands needing password
355     if cmd.requires_password:
356         if wallet.seed == '':
357             seed = ''
358             password = None
359         elif wallet.use_encryption:
360             password = prompt_password('Password:', False)
361             if not password:
362                 print_msg("Error: Password required")
363                 sys.exit(1)
364             # check password
365             try:
366                 seed = wallet.get_seed(password)
367             except Exception:
368                 print_msg("Error: This password does not decode this wallet.")
369                 sys.exit(1)
370         else:
371             password = None
372             seed = wallet.get_seed(None)
373     else:
374         password = None
375
376     # add missing arguments, do type conversions
377     if cmd.name == 'importprivkey':
378         # See if they specificed a key on the cmd line, if not prompt
379         if len(args) == 1:
380             args[1] = prompt_password('Enter PrivateKey (will not echo):', False)
381
382     elif cmd.name == 'signrawtransaction':
383         args = [cmd, args[1], json.loads(args[2]) if len(args) > 2 else [], json.loads(args[3]) if len(args) > 3 else []]
384
385     elif cmd.name == 'createmultisig':
386         args = [cmd, int(args[1]), json.loads(args[2])]
387
388     elif cmd.name == 'createrawtransaction':
389         args = [cmd, json.loads(args[1]), json.loads(args[2])]
390
391     elif cmd.name == 'listaddresses':
392         args = [cmd, options.show_all, options.show_labels]
393
394     elif cmd.name in ['payto', 'mktx']:
395         domain = [options.from_addr] if options.from_addr else None
396         args = ['mktx', args[1], Decimal(args[2]), Decimal(options.tx_fee) if options.tx_fee else None, options.change_addr, domain]
397
398     elif cmd.name in ['paytomany', 'mksendmanytx']:
399         domain = [options.from_addr] if options.from_addr else None
400         outputs = []
401         for i in range(1, len(args), 2):
402             if len(args) < i+2:
403                 print_msg("Error: Mismatched arguments.")
404                 sys.exit(1)
405             outputs.append((args[i], Decimal(args[i+1])))
406         args = ['mksendmanytx', outputs, Decimal(options.tx_fee) if options.tx_fee else None, options.change_addr, domain]
407
408     elif cmd.name == 'help':
409         if len(args) < 2:
410             print_help(parser)
411
412     # check the number of arguments
413     if len(args) - 1 < cmd.min_args:
414         print_msg("Not enough arguments")
415         print_msg("Syntax:", cmd.syntax)
416         sys.exit(1)
417
418     if cmd.max_args >= 0 and len(args) - 1 > cmd.max_args:
419         print_msg("too many arguments", args)
420         print_msg("Syntax:", cmd.syntax)
421         sys.exit(1)
422
423     if cmd.max_args < 0:
424         if len(args) > cmd.min_args + 1:
425             message = ' '.join(args[cmd.min_args:])
426             print_msg("Warning: Final argument was reconstructed from several arguments:", repr(message))
427             args = args[0:cmd.min_args] + [message]
428
429
430
431     # run the command
432     if cmd.name == 'deseed':
433         if not wallet.seed:
434             print_msg("Error: This wallet has no seed")
435         else:
436             ns = wallet.storage.path + '.seedless'
437             print_msg("Warning: you are going to create a seedless wallet'\nIt will be saved in '%s'" % ns)
438             if raw_input("Are you sure you want to continue? (y/n) ") in ['y', 'Y', 'yes']:
439                 wallet.storage.path = ns
440                 wallet.seed = ''
441                 wallet.storage.put('seed', '', True)
442                 wallet.use_encryption = False
443                 wallet.storage.put('use_encryption', wallet.use_encryption, True)
444                 for k in wallet.imported_keys.keys():
445                     wallet.imported_keys[k] = ''
446                 wallet.storage.put('imported_keys', wallet.imported_keys, True)
447                 print_msg("Done.")
448             else:
449                 print_msg("Action canceled.")
450
451     elif cmd.name == 'getconfig':
452         key = args[1]
453         out = config.get(key)
454         print_msg(out)
455
456     elif cmd.name == 'setconfig':
457         key, value = args[1:3]
458         try:
459             value = ast.literal_eval(value)
460         except:
461             pass
462         config.set_key(key, value, True)
463         print_msg(True)
464
465     elif cmd.name == 'password':
466         new_password = prompt_password('New password:')
467         wallet.update_password(password, new_password)
468
469     else:
470         run_command(cmd, password, args)
471
472
473     time.sleep(0.1)
474     sys.exit(0)