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