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