WalletSynchronizer had a race condition caused by calling the callback before the...
[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>\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     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         interface = WalletSynchronizer(wallet, True, gui.server_list_changed)
177         interface.start()
178
179         try:
180             found = wallet.file_exists
181             if not found:
182                 found = gui.restore_or_create()
183         except SystemExit, e:
184             exit(e)
185         except BaseException, e:
186             import traceback
187             traceback.print_exc(file=sys.stdout)
188             #gui.show_message(e.message)
189             exit(1)
190
191         if not found:
192             exit(1)
193         gui.main(url)
194         wallet.save()
195         sys.exit(0)
196
197     if cmd not in known_commands:
198         cmd = 'help'
199
200     if not wallet.file_exists and cmd not in ['help','create','restore']:
201         print "Error: Wallet file not found."
202         print "Type 'electrum create' to create a new wallet, or provide a path to a wallet with the -w option"
203         sys.exit(0)
204     
205     if cmd in ['create', 'restore']:
206         if wallet.file_exists:
207             sys.exit("Error: Remove the existing wallet first!")
208         password = prompt_password("Password (hit return if you do not wish to encrypt your wallet):")
209
210         w_host, w_port, w_protocol = wallet.server.split(':')
211         host = raw_input("server (default:%s):"%w_host)
212         port = raw_input("port (default:%s):"%w_port)
213         protocol = raw_input("protocol [t=tcp;h=http;n=native] (default:%s):"%w_protocol)
214         fee = raw_input("fee (default:%s):"%( str(Decimal(wallet.fee)/100000000)) )
215         gap = raw_input("gap limit (default 5):")
216         if host: w_host = host
217         if port: w_port = port
218         if protocol: w_protocol = protocol
219         wallet.server = w_host + ':' + w_port + ':' +w_protocol
220         if fee: wallet.fee = float(fee)
221         if gap: wallet.gap_limit = int(gap)
222
223         if cmd == 'restore':
224             seed = raw_input("seed:")
225             try:
226                 seed.decode('hex')
227             except:
228                 print_error("Warning: Not hex, trying decode.")
229                 seed = mnemonic.mn_decode( seed.split(' ') )
230             if not seed:
231                 sys.exit("Error: No seed")
232
233             wallet.seed = str(seed)
234             wallet.init_mpk( wallet.seed )
235             if not options.offline:
236                 WalletSynchronizer(wallet).start()
237                 print "Recovering wallet..."
238                 wallet.up_to_date_event.clear()
239                 wallet.up_to_date = False
240                 wallet.update()
241                 if wallet.is_found():
242                     print "Recovery successful"
243                 else:
244                     print_error("Warning: Found no history for this wallet")
245             wallet.fill_addressbook()
246             wallet.save()
247             print_error("Wallet saved in '" + wallet.path)
248         else:
249             wallet.new_seed(None)
250             wallet.init_mpk( wallet.seed )
251             wallet.synchronize() # there is no wallet thread 
252             wallet.save()
253             print "Your wallet generation seed is: " + wallet.seed
254             print "Please keep it in a safe place; if you lose it, you will not be able to restore your wallet."
255             print "Equivalently, your wallet seed can be stored and recovered with the following mnemonic code:"
256             print "\""+' '.join(mnemonic.mn_encode(wallet.seed))+"\""
257             print "Wallet saved in '%s'"%wallet.path
258             
259         if password:
260             wallet.update_password(wallet.seed, None, password)
261
262     # check syntax
263     if cmd in ['payto', 'mktx']:
264         try:
265             to_address = args[1]
266             amount = int( 100000000 * Decimal(args[2]) )
267             change_addr = None
268             label = ' '.join(args[3:])
269             if options.tx_fee: 
270                 options.tx_fee = int( 100000000 * Decimal(options.tx_fee) )
271         except:
272             firstarg = cmd
273             cmd = 'help'
274
275     # open session
276     if cmd not in offline_commands and not options.offline:
277         WalletSynchronizer(wallet).start()
278         wallet.update()
279         wallet.save()
280
281     # check if --from_addr not in wallet (for mktx/payto)
282     is_temporary = False
283     from_addr = None
284     if options.from_addr:
285         from_addr = options.from_addr
286         if from_addr not in wallet.all_addresses():
287             is_temporary = True
288                 
289     # commands needing password
290     if cmd in protected_commands or ( cmd=='addresses' and options.show_keys):
291         password = prompt_password('Password:', False) if wallet.use_encryption and not is_temporary else None
292         # check password
293         try:
294             wallet.pw_decode( wallet.seed, password)
295         except:
296             print_error("Error: This password does not decode this wallet.")
297             exit(1)
298
299     if cmd == 'import':
300         # See if they specificed a key on the cmd line, if not prompt
301         if len(args) > 1:
302             keypair = args[1]
303         else:
304             keypair = prompt_password('Enter Address:PrivateKey (will not echo):', False)
305         try:
306             wallet.import_key(keypair,password)
307             wallet.save()
308             print "Keypair imported"
309         except BaseException, e:
310             print_error("Error: Keypair import failed: " + str(e))
311
312     if cmd == 'help':
313         cmd2 = firstarg
314         if cmd2 not in known_commands:
315             parser.print_help()
316             print "Type 'electrum help <command>' to see the help for a specific command"
317             print "Type 'electrum --help' to see the list of options"
318             print "List of commands:", ', '.join(known_commands)
319         else:
320             print known_commands[cmd2]
321
322     elif cmd == 'seed':
323         seed = wallet.pw_decode( wallet.seed, password)
324         print seed + ' "' + ' '.join(mnemonic.mn_encode(seed)) + '"'
325
326     elif cmd == 'deseed':
327         if not wallet.seed:
328             print_error("Error: This wallet has no seed")
329         elif wallet.use_encryption:
330             print_error("Error: This wallet is encrypted")
331         else:
332             ns = wallet.path + '.seed'
333             print "Warning: you are going to extract the seed from '%s'\nThe seed will be saved in '%s'"%(wallet.path,ns)
334             if raw_input("Are you sure you want to continue? (y/n) ") in ['y','Y','yes']:
335                 f = open(ns,'w')
336                 f.write(repr({'seed':wallet.seed, 'imported_keys':wallet.imported_keys})+"\n")
337                 f.close()
338                 wallet.seed = ''
339                 for k in wallet.imported_keys.keys(): wallet.imported_keys[k] = ''
340                 wallet.save()
341                 print "Done."
342             else:
343                 print_error("Action canceled.")
344
345     elif cmd == 'reseed':
346         if wallet.seed:
347             print "Warning: This wallet already has a seed", wallet.seed
348         else:
349             ns = wallet.path + '.seed'
350             try:
351                 f = open(ns,'r')
352                 data = f.read()
353                 f.close()
354             except IOError:
355                 sys.exit("Error: Seed file not found")
356             try:
357                 import ast
358                 d = ast.literal_eval( data )
359                 seed = d['seed']
360                 imported_keys = d.get('imported_keys',{})
361             except:
362                 sys.exit("Error: Error with seed file")
363
364             mpk = wallet.master_public_key
365             wallet.seed = seed
366             wallet.imported_keys = imported_keys
367             wallet.use_encryption = False
368             wallet.init_mpk(seed)
369             if mpk == wallet.master_public_key:
370                 wallet.save()
371                 print "Done: " + wallet.path
372             else:
373                 print_error("Error: Master public key does not match")
374
375     elif cmd == 'validateaddress':
376         addr = args[1]
377         print wallet.is_valid(addr)
378
379     elif cmd == 'balance':
380         try:
381             addrs = args[1:]
382         except:
383             pass
384         if addrs == []:
385             c, u = wallet.get_balance()
386             if u:
387                 print Decimal( c ) / 100000000 , Decimal( u ) / 100000000
388             else:
389                 print Decimal( c ) / 100000000
390         else:
391             for addr in addrs:
392                 c, u = wallet.get_addr_balance(addr)
393                 if u:
394                     print "%s %s, %s" % (addr, str(Decimal(c)/100000000), str(Decimal(u)/100000000))
395                 else:
396                     print "%s %s" % (addr, str(Decimal(c)/100000000))
397
398     elif cmd in [ 'contacts']:
399         for addr in wallet.addressbook:
400             print addr, "   ", wallet.labels.get(addr)
401
402     elif cmd == 'eval':
403         print eval(args[1])
404         wallet.save()
405
406     elif cmd in [ 'addresses']:
407         for addr in wallet.all_addresses():
408             if options.show_all or not wallet.is_change(addr):
409
410                 flags = wallet.get_address_flags(addr)
411                 label = wallet.labels.get(addr,'')
412                 
413                 if label: label = "\"%s\""%label
414
415                 if options.show_balance:
416                     h = wallet.history.get(addr,[])
417                     #ni = no = 0
418                     #for item in h:
419                     #    if item['is_input']:  ni += 1
420                     #    else:              no += 1
421                     b = format_satoshis(wallet.get_addr_balance(addr)[0])
422                 else: b=''
423                 m_addr = "%34s"%addr
424                 if options.show_keys:
425                     m_addr += ':' + str(wallet.get_private_key_base58(addr, password))
426                 print flags, m_addr, b, label
427
428     if cmd == 'history':
429         lines = wallet.get_tx_history()
430         b = 0 
431         for line in lines:
432             import datetime
433             v = line['value'] 
434             b += v
435             try:
436                 time_str = str( datetime.datetime.fromtimestamp( line['timestamp']))
437             except:
438                 print line['timestamp']
439                 time_str = 'pending'
440             label = line.get('label')
441             if not label: label = line['tx_hash']
442             else: label = label + ' '*(64 - len(label) )
443
444             print time_str , "  " + label + "  " + format_satoshis(v)+ "  "+ format_satoshis(b)
445         print "# balance: ", format_satoshis(b)
446
447     elif cmd == 'label':
448         try:
449             tx = args[1]
450             label = ' '.join(args[2:])
451         except:
452             print_error("Error. Syntax:  label <tx_hash> <text>")
453             sys.exit(1)
454         wallet.labels[tx] = label
455         wallet.save()
456             
457     elif cmd in ['payto', 'mktx']:
458         if from_addr and is_temporary:
459             if from_addr.find(":") == -1:
460                 keypair = from_addr + ":" + prompt_password('Private key:', False)
461             else:
462                 keypair = from_addr
463                 from_addr = keypair.split(':')[0]
464             if not wallet.import_key(keypair,password):
465                 print_error("Error: Invalid key pair")
466                 exit(1)
467             wallet.history[from_addr] = interface.retrieve_history(from_addr)
468             wallet.update_tx_history()
469             change_addr = from_addr
470
471         if options.change_addr:
472             change_addr = options.change_addr
473
474         for k, v in wallet.labels.items():
475             if v == to_address:
476                 to_address = k
477                 print "alias", to_address
478                 break
479             if change_addr and v == change_addr:
480                 change_addr = k
481         try:
482             tx = wallet.mktx( to_address, amount, label, password,
483                 fee = options.tx_fee, change_addr = change_addr, from_addr = from_addr )
484         except:
485             import traceback
486             traceback.print_exc(file=sys.stdout)
487             tx = None
488
489         if tx and cmd=='payto': 
490             r, h = wallet.sendtx( tx )
491             print h
492         else:
493             print tx
494
495         if is_temporary:
496             wallet.imported_keys.pop(from_addr)
497             del(wallet.history[from_addr])
498         wallet.save()
499
500     elif cmd == 'sendtx':
501         tx = args[1]
502         r, h = wallet.sendtx( tx )
503         print h
504
505     elif cmd == 'password':
506         try:
507             seed = wallet.pw_decode( wallet.seed, password)
508         except ValueError:
509             sys.exit("Error: Password does not decrypt this wallet.")
510
511         new_password = prompt_password('New password:')
512         wallet.update_password(seed, password, new_password)
513
514     elif cmd == 'signmessage':
515         if len(args) < 3:
516             print_error("Error: Invalid usage of signmessage.")
517             print known_commands[cmd]
518             sys.exit(1)
519         address = args[1]
520         message = ' '.join(args[2:])
521         if len(args) > 3:
522             print "Warning: Message was reconstructed from several arguments:", repr(message)
523         print wallet.sign_message(address, message, password)
524
525     elif cmd == 'verifymessage':
526         try:
527             address = args[1]
528             signature = args[2]
529             message = ' '.join(args[3:])
530         except:
531             print_error("Error: Not all parameters were given, displaying help instead.")
532             print known_commands[cmd]
533             sys.exit(1)
534         if len(args) > 4:
535             print "Warning: Message was reconstructed from several arguments:", repr(message)
536         try:
537             wallet.verify_message(address, signature, message)
538             print True
539         except BaseException as e:
540             print "Verification error: {0}".format(e)
541             print False
542
543     elif cmd == 'freeze':
544         addr = args[1]
545         print self.wallet.freeze(addr)
546         
547     elif cmd == 'unfreeze':
548         addr = args[1]
549         print self.wallet.unfreeze(addr)
550
551     elif cmd == 'prioritize':
552         addr = args[1]
553         print self.wallet.prioritize(addr)
554
555     elif cmd == 'unprioritize':
556         addr = args[1]
557         print self.wallet.unprioritize(addr)
558