Added SimpleConfig class to deal with simple config options added for fallback to...
[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 import re
20 import sys
21 # import argparse
22 import optparse
23
24 try:
25     from lib.util import print_error
26 except ImportError:
27     from electrum.util import print_error
28
29 try:
30     import ecdsa  
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
36 except ImportError:
37     sys.exit("Error: AES does not seem to be installed. Try 'sudo pip install slowaes'")
38
39 try:
40     from lib import Wallet, WalletSynchronizer, format_satoshis, mnemonic, prompt_password
41 except ImportError:
42     from electrum import Wallet, WalletSynchronizer, format_satoshis, mnemonic, prompt_password
43
44 from decimal import Decimal
45 from lib import SimpleConfig
46
47 known_commands = {
48     'help':'Prints this help',
49     'validateaddress':'Check that the address is valid', 
50     'balance': "Display the balance of your wallet or of an address.\nSyntax: balance [<address>]", 
51     'contacts': "Show your list of contacts", 
52     'create':'Create a wallet', 
53     'restore':'Restore a wallet', 
54     'payto':"""Create and broadcast a transaction.
55 Syntax: payto <recipient> <amount> [label]
56 <recipient> can be a bitcoin address or a label
57 options:\n  --fee, -f: set transaction fee\n  --fromaddr, -s: send from address -\n  --changeaddr, -c: send change to address
58             """,
59     'sendtx':
60             'Broadcasts a transaction to the network. \nSyntax: sendtx <tx>\n<tx> must be in hexadecimal.',
61     'password': 
62             "Changes your password",
63     'addresses':  
64             """Shows your list of addresses.
65 options:
66   -a: show all addresses, including change addresses
67   -k: show private keys
68   -b: show the balance of addresses""",
69
70     'history':"Shows the transaction history",
71     'label':'Assign a label to an item\nSyntax: label <tx_hash> <label>',
72     'mktx':
73         """Create a signed transaction, password protected.
74 Syntax: mktx <recipient> <amount> [label]
75 options:\n  --fee, -f: set transaction fee\n  --fromaddr, -s: send from address -\n  --changeaddr, -c: send change to address
76         """,
77     'seed':
78             "Print the generation seed of your wallet.",
79     'import': 
80             'Imports a key pair\nSyntax: import <address>:<privatekey>',
81     'signmessage':
82             'Signs a message with a key\nSyntax: signmessage <address> <message>\nIf you want to lead or end a message with spaces, or want double spaces inside the message make sure you quote the string. I.e. " Hello  This is a weird String "',
83     'verifymessage':
84              'Verifies a signature\nSyntax: verifymessage <address> <signature> <message>\nIf you want to lead or end a message with spaces, or want double spaces inside the message make sure you quote the string. I.e. " Hello  This is a weird String "',
85     'eval':  
86              "Run python eval() on an object\nSyntax: eval <expression>\nExample: eval \"wallet.aliases\"",
87     'deseed':
88             "Remove seed from the wallet. The seed is stored in a file that has the name of the wallet plus '.seed'",
89     'reseed':
90             "Restore seed of the wallet. The wallet must have no seed, and the seed must match the wallet's master public key.",
91     'freeze':'',
92     'unfreeze':'',
93     'prioritize':'',
94     'unprioritize':'',
95     }
96
97
98
99 offline_commands = [ 'password', 'mktx', 'label', 'contacts', 'help', 'validateaddress', 'signmessage', 'verifymessage', 'eval', 'create', 'addresses', 'import', 'seed','deseed','reseed','freeze','unfreeze','prioritize','unprioritize']
100
101 protected_commands = ['payto', 'password', 'mktx', 'seed', 'import','signmessage' ]
102
103 if __name__ == '__main__':
104
105     # Load simple config class
106     simple_config = SimpleConfig()
107
108     usage = "usage: %prog [options] command\nCommands: "+ (', '.join(known_commands))
109     parser = optparse.OptionParser(prog=usage)
110     parser.add_option("-g", "--gui", dest="gui", default=simple_config.config["gui"], help="gui")
111     parser.add_option("-w", "--wallet", dest="wallet_path", help="wallet path (default: electrum.dat)")
112     parser.add_option("-o", "--offline", action="store_true", dest="offline", default=False, help="remain offline")
113     parser.add_option("-a", "--all", action="store_true", dest="show_all", default=False, help="show all addresses")
114     parser.add_option("-b", "--balance", action="store_true", dest="show_balance", default=False, help="show the balance at listed addresses")
115     parser.add_option("-k", "--keys",action="store_true", dest="show_keys",default=False, help="show the private keys of listed addresses")
116     parser.add_option("-f", "--fee", dest="tx_fee", default="0.005", help="set tx fee")
117     parser.add_option("-s", "--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.")
118     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")
119     parser.add_option("-r", "--remote", dest="remote_url", default=None, help="URL of a remote wallet")
120     options, args = parser.parse_args()
121
122
123     wallet = Wallet()
124     wallet.set_path(options.wallet_path)
125     wallet.read()
126     wallet.remote_url = options.remote_url
127
128     if len(args)==0:
129         url = None
130         cmd = 'gui'
131     elif len(args)==1 and re.match('^bitcoin:', args[0]):
132         url = args[0]
133         cmd = 'gui'
134     else:
135         cmd = args[0]
136         firstarg = args[1] if len(args) > 1 else ''
137        
138     #this entire if/else block is just concerned with importing the 
139     #right GUI toolkit based the GUI command line option given 
140     if cmd == 'gui':
141         
142         if options.gui=='gtk':
143             try:
144                 import lib.gui as gui
145             except ImportError:
146                 import electrum.gui as gui
147         elif options.gui=='qt':
148             try:
149                 import lib.gui_qt as gui
150             except ImportError:
151                 import electrum.gui_qt as gui
152         elif options.gui == 'lite':
153             # Let's do some dep checking and handle missing ones gracefully
154             try:
155               from PyQt4.QtCore import *
156               from PyQt4.QtGui import *
157             except ImportError:
158               print "You need to have PyQT installed to run Electrum in graphical mode."
159               print "If you have pip installed try 'sudo pip install pyqt' if you are on Debian/Ubuntu try 'sudo apt-get install python-qt4'."
160               sys.exit(0)
161
162             qtVersion = qVersion()
163             if not(int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
164               app = QApplication(sys.argv)
165
166               error_message = QErrorMessage()
167               error_message.setFixedSize(350,200)
168               error_message.showMessage("<p>Sorry, the Electrum 'Lite GUI' requires Qt >= 4.7 to run. The pro GUI will be started instead.</p><p>Check your distributions packages for upgrades.</p>")
169               simple_config.config["gui"] = "qt"
170               simple_config.save_config
171               app.exec_()
172
173               try:
174                   import lib.gui_qt as gui
175               except ImportError:
176                   import electrum.gui_qt as gui
177             else:
178               #use the lite version if no toolkit specified
179               try:
180                 import lib.gui_lite as gui
181               except ImportError:
182                 import electrum.gui_lite as gui
183         else:
184             sys.exit("Error: Unknown GUI: " + options.gui)
185
186         gui = gui.ElectrumGui(wallet)
187         interface = WalletSynchronizer(wallet, True, gui.server_list_changed)
188         interface.start()
189
190         try:
191             found = wallet.file_exists
192             if not found:
193                 found = gui.restore_or_create()
194         except SystemExit, e:
195             exit(e)
196         except BaseException, e:
197             import traceback
198             traceback.print_exc(file=sys.stdout)
199             #gui.show_message(e.message)
200             exit(1)
201
202         if not found:
203             exit(1)
204         gui.main(url)
205         wallet.save()
206         sys.exit(0)
207
208     if cmd not in known_commands:
209         cmd = 'help'
210
211     if not wallet.file_exists and cmd not in ['help','create','restore']:
212         print "Error: Wallet file not found."
213         print "Type 'electrum create' to create a new wallet, or provide a path to a wallet with the -w option"
214         sys.exit(0)
215     
216     if cmd in ['create', 'restore']:
217         if wallet.file_exists:
218             sys.exit("Error: Remove the existing wallet first!")
219         password = prompt_password("Password (hit return if you do not wish to encrypt your wallet):")
220
221         w_host, w_port, w_protocol = wallet.server.split(':')
222         host = raw_input("server (default:%s):"%w_host)
223         port = raw_input("port (default:%s):"%w_port)
224         protocol = raw_input("protocol [t=tcp;h=http;n=native] (default:%s):"%w_protocol)
225         fee = raw_input("fee (default:%s):"%( str(Decimal(wallet.fee)/100000000)) )
226         gap = raw_input("gap limit (default 5):")
227         if host: w_host = host
228         if port: w_port = port
229         if protocol: w_protocol = protocol
230         wallet.server = w_host + ':' + w_port + ':' +w_protocol
231         if fee: wallet.fee = float(fee)
232         if gap: wallet.gap_limit = int(gap)
233
234         if cmd == 'restore':
235             seed = raw_input("seed:")
236             try:
237                 seed.decode('hex')
238             except:
239                 print_error("Warning: Not hex, trying decode.")
240                 seed = mnemonic.mn_decode( seed.split(' ') )
241             if not seed:
242                 sys.exit("Error: No seed")
243
244             wallet.seed = str(seed)
245             wallet.init_mpk( wallet.seed )
246             if not options.offline:
247                 WalletSynchronizer(wallet).start()
248                 print "Recovering wallet..."
249                 wallet.up_to_date_event.clear()
250                 wallet.up_to_date = False
251                 wallet.update()
252                 if wallet.is_found():
253                     print "Recovery successful"
254                 else:
255                     print_error("Warning: Found no history for this wallet")
256             wallet.fill_addressbook()
257             wallet.save()
258             print_error("Wallet saved in '" + wallet.path)
259         else:
260             wallet.new_seed(None)
261             wallet.init_mpk( wallet.seed )
262             wallet.synchronize() # there is no wallet thread 
263             wallet.save()
264             print "Your wallet generation seed is: " + wallet.seed
265             print "Please keep it in a safe place; if you lose it, you will not be able to restore your wallet."
266             print "Equivalently, your wallet seed can be stored and recovered with the following mnemonic code:"
267             print "\""+' '.join(mnemonic.mn_encode(wallet.seed))+"\""
268             print "Wallet saved in '%s'"%wallet.path
269             
270         if password:
271             wallet.update_password(wallet.seed, None, password)
272
273     # check syntax
274     if cmd in ['payto', 'mktx']:
275         try:
276             to_address = args[1]
277             amount = int( 100000000 * Decimal(args[2]) )
278             change_addr = None
279             label = ' '.join(args[3:])
280             if options.tx_fee: 
281                 options.tx_fee = int( 100000000 * Decimal(options.tx_fee) )
282         except:
283             firstarg = cmd
284             cmd = 'help'
285
286     # open session
287     if cmd not in offline_commands and not options.offline:
288         WalletSynchronizer(wallet).start()
289         wallet.update()
290         wallet.save()
291
292     # check if --from_addr not in wallet (for mktx/payto)
293     is_temporary = False
294     from_addr = None
295     if options.from_addr:
296         from_addr = options.from_addr
297         if from_addr not in wallet.all_addresses():
298             is_temporary = True
299                 
300     # commands needing password
301     if cmd in protected_commands or ( cmd=='addresses' and options.show_keys):
302         password = prompt_password('Password:', False) if wallet.use_encryption and not is_temporary else None
303         # check password
304         try:
305             wallet.pw_decode( wallet.seed, password)
306         except:
307             print_error("Error: This password does not decode this wallet.")
308             exit(1)
309
310     if cmd == 'import':
311         # See if they specificed a key on the cmd line, if not prompt
312         if len(args) > 1:
313             keypair = args[1]
314         else:
315             keypair = prompt_password('Enter Address:PrivateKey (will not echo):', False)
316         try:
317             wallet.import_key(keypair,password)
318             wallet.save()
319             print "Keypair imported"
320         except BaseException, e:
321             print_error("Error: Keypair import failed: " + str(e))
322
323     if cmd == 'help':
324         cmd2 = firstarg
325         if cmd2 not in known_commands:
326             parser.print_help()
327             print "Type 'electrum help <command>' to see the help for a specific command"
328             print "Type 'electrum --help' to see the list of options"
329             print "List of commands:", ', '.join(known_commands)
330         else:
331             print known_commands[cmd2]
332
333     elif cmd == 'seed':
334         seed = wallet.pw_decode( wallet.seed, password)
335         print seed + ' "' + ' '.join(mnemonic.mn_encode(seed)) + '"'
336
337     elif cmd == 'deseed':
338         if not wallet.seed:
339             print_error("Error: This wallet has no seed")
340         elif wallet.use_encryption:
341             print_error("Error: This wallet is encrypted")
342         else:
343             ns = wallet.path + '.seed'
344             print "Warning: you are going to extract the seed from '%s'\nThe seed will be saved in '%s'"%(wallet.path,ns)
345             if raw_input("Are you sure you want to continue? (y/n) ") in ['y','Y','yes']:
346                 f = open(ns,'w')
347                 f.write(repr({'seed':wallet.seed, 'imported_keys':wallet.imported_keys})+"\n")
348                 f.close()
349                 wallet.seed = ''
350                 for k in wallet.imported_keys.keys(): wallet.imported_keys[k] = ''
351                 wallet.save()
352                 print "Done."
353             else:
354                 print_error("Action canceled.")
355
356     elif cmd == 'reseed':
357         if wallet.seed:
358             print "Warning: This wallet already has a seed", wallet.seed
359         else:
360             ns = wallet.path + '.seed'
361             try:
362                 f = open(ns,'r')
363                 data = f.read()
364                 f.close()
365             except IOError:
366                 sys.exit("Error: Seed file not found")
367             try:
368                 import ast
369                 d = ast.literal_eval( data )
370                 seed = d['seed']
371                 imported_keys = d.get('imported_keys',{})
372             except:
373                 sys.exit("Error: Error with seed file")
374
375             mpk = wallet.master_public_key
376             wallet.seed = seed
377             wallet.imported_keys = imported_keys
378             wallet.use_encryption = False
379             wallet.init_mpk(seed)
380             if mpk == wallet.master_public_key:
381                 wallet.save()
382                 print "Done: " + wallet.path
383             else:
384                 print_error("Error: Master public key does not match")
385
386     elif cmd == 'validateaddress':
387         addr = args[1]
388         print wallet.is_valid(addr)
389
390     elif cmd == 'balance':
391         try:
392             addrs = args[1:]
393         except:
394             pass
395         if addrs == []:
396             c, u = wallet.get_balance()
397             if u:
398                 print Decimal( c ) / 100000000 , Decimal( u ) / 100000000
399             else:
400                 print Decimal( c ) / 100000000
401         else:
402             for addr in addrs:
403                 c, u = wallet.get_addr_balance(addr)
404                 if u:
405                     print "%s %s, %s" % (addr, str(Decimal(c)/100000000), str(Decimal(u)/100000000))
406                 else:
407                     print "%s %s" % (addr, str(Decimal(c)/100000000))
408
409     elif cmd in [ 'contacts']:
410         for addr in wallet.addressbook:
411             print addr, "   ", wallet.labels.get(addr)
412
413     elif cmd == 'eval':
414         print eval(args[1])
415         wallet.save()
416
417     elif cmd in [ 'addresses']:
418         for addr in wallet.all_addresses():
419             if options.show_all or not wallet.is_change(addr):
420
421                 flags = wallet.get_address_flags(addr)
422                 label = wallet.labels.get(addr,'')
423                 
424                 if label: label = "\"%s\""%label
425
426                 if options.show_balance:
427                     h = wallet.history.get(addr,[])
428                     #ni = no = 0
429                     #for item in h:
430                     #    if item['is_input']:  ni += 1
431                     #    else:              no += 1
432                     b = format_satoshis(wallet.get_addr_balance(addr)[0])
433                 else: b=''
434                 m_addr = "%34s"%addr
435                 if options.show_keys:
436                     m_addr += ':' + str(wallet.get_private_key_base58(addr, password))
437                 print flags, m_addr, b, label
438
439     if cmd == 'history':
440         lines = wallet.get_tx_history()
441         b = 0 
442         for line in lines:
443             import datetime
444             v = line['value'] 
445             b += v
446             try:
447                 time_str = str( datetime.datetime.fromtimestamp( line['timestamp']))
448             except:
449                 print line['timestamp']
450                 time_str = 'pending'
451             label = line.get('label')
452             if not label: label = line['tx_hash']
453             else: label = label + ' '*(64 - len(label) )
454
455             print time_str , "  " + label + "  " + format_satoshis(v)+ "  "+ format_satoshis(b)
456         print "# balance: ", format_satoshis(b)
457
458     elif cmd == 'label':
459         try:
460             tx = args[1]
461             label = ' '.join(args[2:])
462         except:
463             print_error("Error. Syntax:  label <tx_hash> <text>")
464             sys.exit(1)
465         wallet.labels[tx] = label
466         wallet.save()
467             
468     elif cmd in ['payto', 'mktx']:
469         if from_addr and is_temporary:
470             if from_addr.find(":") == -1:
471                 keypair = from_addr + ":" + prompt_password('Private key:', False)
472             else:
473                 keypair = from_addr
474                 from_addr = keypair.split(':')[0]
475             if not wallet.import_key(keypair,password):
476                 print_error("Error: Invalid key pair")
477                 exit(1)
478             wallet.history[from_addr] = interface.retrieve_history(from_addr)
479             wallet.update_tx_history()
480             change_addr = from_addr
481
482         if options.change_addr:
483             change_addr = options.change_addr
484
485         for k, v in wallet.labels.items():
486             if v == to_address:
487                 to_address = k
488                 print "alias", to_address
489                 break
490             if change_addr and v == change_addr:
491                 change_addr = k
492         try:
493             tx = wallet.mktx( to_address, amount, label, password,
494                 fee = options.tx_fee, change_addr = change_addr, from_addr = from_addr )
495         except:
496             import traceback
497             traceback.print_exc(file=sys.stdout)
498             tx = None
499
500         if tx and cmd=='payto': 
501             r, h = wallet.sendtx( tx )
502             print h
503         else:
504             print tx
505
506         if is_temporary:
507             wallet.imported_keys.pop(from_addr)
508             del(wallet.history[from_addr])
509         wallet.save()
510
511     elif cmd == 'sendtx':
512         tx = args[1]
513         r, h = wallet.sendtx( tx )
514         print h
515
516     elif cmd == 'password':
517         try:
518             seed = wallet.pw_decode( wallet.seed, password)
519         except ValueError:
520             sys.exit("Error: Password does not decrypt this wallet.")
521
522         new_password = prompt_password('New password:')
523         wallet.update_password(seed, password, new_password)
524
525     elif cmd == 'signmessage':
526         if len(args) < 3:
527             print_error("Error: Invalid usage of signmessage.")
528             print known_commands[cmd]
529             sys.exit(1)
530         address = args[1]
531         message = ' '.join(args[2:])
532         if len(args) > 3:
533             print "Warning: Message was reconstructed from several arguments:", repr(message)
534         print wallet.sign_message(address, message, password)
535
536     elif cmd == 'verifymessage':
537         try:
538             address = args[1]
539             signature = args[2]
540             message = ' '.join(args[3:])
541         except:
542             print_error("Error: Not all parameters were given, displaying help instead.")
543             print known_commands[cmd]
544             sys.exit(1)
545         if len(args) > 4:
546             print "Warning: Message was reconstructed from several arguments:", repr(message)
547         try:
548             wallet.verify_message(address, signature, message)
549             print True
550         except BaseException as e:
551             print "Verification error: {0}".format(e)
552             print False
553
554     elif cmd == 'freeze':
555         addr = args[1]
556         print self.wallet.freeze(addr)
557         
558     elif cmd == 'unfreeze':
559         addr = args[1]
560         print self.wallet.unfreeze(addr)
561
562     elif cmd == 'prioritize':
563         addr = args[1]
564         print self.wallet.prioritize(addr)
565
566     elif cmd == 'unprioritize':
567         addr = args[1]
568         print self.wallet.unprioritize(addr)
569