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