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