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