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