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