62f3b659fa78259eafb06eedab02bc91cbd19cdb
[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         
203         password = prompt_password("Password (hit return if you do not wish to encrypt your wallet):")
204
205         w_host, w_port, w_protocol = wallet.server.split(':')
206         host = raw_input("server (default:%s):"%w_host)
207         port = raw_input("port (default:%s):"%w_port)
208         protocol = raw_input("protocol [t=tcp;h=http;n=native] (default:%s):"%w_protocol)
209         fee = raw_input("fee (default:%s):"%( str(Decimal(wallet.fee)/100000000)) )
210         gap = raw_input("gap limit (default 5):")
211         if host: w_host = host
212         if port: w_port = port
213         if protocol: w_protocol = protocol
214         wallet.server = w_host + ':' + w_port + ':' +w_protocol
215         if fee: wallet.fee = float(fee)
216         if gap: wallet.gap_limit = int(gap)
217
218         if cmd == 'restore':
219             seed = raw_input("seed:")
220             try:
221                 seed.decode('hex')
222             except:
223                 print_error("Warning: Not hex, trying decode.")
224                 seed = mnemonic.mn_decode( seed.split(' ') )
225             if not seed:
226                 sys.exit("Error: No seed")
227
228             wallet.seed = str(seed)
229             wallet.init_mpk( wallet.seed )
230             if not options.offline:
231                 WalletSynchronizer(wallet).start()
232                 print "Recovering wallet..."
233                 wallet.up_to_date_event.clear()
234                 wallet.up_to_date = False
235                 wallet.update()
236                 if wallet.is_found():
237                     print "Recovery successful"
238                 else:
239                     print_error("Warning: Found no history for this wallet")
240             wallet.fill_addressbook()
241             wallet.save()
242             print_error("Wallet saved in '" + wallet.path)
243         else:
244             wallet.new_seed(None)
245             wallet.init_mpk( wallet.seed )
246             wallet.synchronize() # there is no wallet thread 
247             wallet.save()
248             print "Your wallet generation seed is: " + wallet.seed
249             print "Please keep it in a safe place; if you lose it, you will not be able to restore your wallet."
250             print "Equivalently, your wallet seed can be stored and recovered with the following mnemonic code:"
251             print "\""+' '.join(mnemonic.mn_encode(wallet.seed))+"\""
252             print "Wallet saved in '%s'"%wallet.path
253             
254         if password:
255             wallet.update_password(wallet.seed, None, password)
256
257     # check syntax
258     if cmd in ['payto', 'mktx']:
259         try:
260             to_address = args[1]
261             amount = int( 100000000 * Decimal(args[2]) )
262             change_addr = None
263             label = ' '.join(args[3:])
264             if options.tx_fee: 
265                 options.tx_fee = int( 100000000 * Decimal(options.tx_fee) )
266         except:
267             firstarg = cmd
268             cmd = 'help'
269
270     # open session
271     if cmd not in offline_commands and not options.offline:
272         WalletSynchronizer(wallet).start()
273         wallet.update()
274         wallet.save()
275
276     # check if --from_addr not in wallet (for mktx/payto)
277     is_temporary = False
278     from_addr = None
279     if options.from_addr:
280         from_addr = options.from_addr
281         if from_addr not in wallet.all_addresses():
282             is_temporary = True
283                 
284     # commands needing password
285     if cmd in protected_commands or ( cmd=='addresses' and options.show_keys):
286         password = prompt_password('Password:', False) if wallet.use_encryption and not is_temporary else None
287         # check password
288         try:
289             wallet.pw_decode( wallet.seed, password)
290         except:
291             print_error("Error: This password does not decode this wallet.")
292             exit(1)
293
294     if cmd == 'import':
295         # See if they specificed a key on the cmd line, if not prompt
296         if len(args) > 1:
297             keypair = args[1]
298         else:
299             keypair = prompt_password('Enter Address:PrivateKey (will not echo):', False)
300         try:
301             wallet.import_key(keypair,password)
302             wallet.save()
303             print "Keypair imported"
304         except BaseException, e:
305             print_error("Error: Keypair import failed: " + str(e))
306
307     if cmd == 'help':
308         cmd2 = firstarg
309         if cmd2 not in known_commands:
310             parser.print_help()
311             print
312             print "Type 'electrum help <command>' to see the help for a specific command"
313             print "Type 'electrum --help' to see the list of options"
314             print "List of commands:", ', '.join(known_commands)
315         else:
316             print known_commands[cmd2]
317
318     elif cmd == 'seed':
319         seed = wallet.pw_decode( wallet.seed, password)
320         print seed + ' "' + ' '.join(mnemonic.mn_encode(seed)) + '"'
321
322     elif cmd == 'deseed':
323         if not wallet.seed:
324             print_error("Error: This wallet has no seed")
325         elif wallet.use_encryption:
326             print_error("Error: This wallet is encrypted")
327         else:
328             ns = wallet.path + '.seed'
329             print "Warning: you are going to extract the seed from '%s'\nThe seed will be saved in '%s'"%(wallet.path,ns)
330             if raw_input("Are you sure you want to continue? (y/n) ") in ['y','Y','yes']:
331                 f = open(ns,'w')
332                 f.write(repr({'seed':wallet.seed, 'imported_keys':wallet.imported_keys})+"\n")
333                 f.close()
334                 wallet.seed = ''
335                 for k in wallet.imported_keys.keys(): wallet.imported_keys[k] = ''
336                 wallet.save()
337                 print "Done."
338             else:
339                 print_error("Action canceled.")
340
341     elif cmd == 'reseed':
342         if wallet.seed:
343             print "Warning: This wallet already has a seed", wallet.seed
344         else:
345             ns = wallet.path + '.seed'
346             try:
347                 f = open(ns,'r')
348                 data = f.read()
349                 f.close()
350             except IOError:
351                 sys.exit("Error: Seed file not found")
352             try:
353                 import ast
354                 d = ast.literal_eval( data )
355                 seed = d['seed']
356                 imported_keys = d.get('imported_keys',{})
357             except IOError:
358                 sys.exit("Error: Error with seed file")
359
360             mpk = wallet.master_public_key
361             wallet.seed = seed
362             wallet.imported_keys = imported_keys
363             wallet.use_encryption = False
364             wallet.init_mpk(seed)
365             if mpk == wallet.master_public_key:
366                 wallet.save()
367                 print "Done: " + wallet.path
368             else:
369                 print_error("Error: Master public key does not match")
370
371     elif cmd == 'validateaddress':
372         addr = args[1]
373         print wallet.is_valid(addr)
374
375     elif cmd == 'balance':
376         try:
377             addrs = args[1:]
378         except:
379             pass
380         if addrs == []:
381             c, u = wallet.get_balance()
382             if u:
383                 print Decimal( c ) / 100000000 , Decimal( u ) / 100000000
384             else:
385                 print Decimal( c ) / 100000000
386         else:
387             for addr in addrs:
388                 c, u = wallet.get_addr_balance(addr)
389                 if u:
390                     print "%s %s, %s" % (addr, str(Decimal(c)/100000000), str(Decimal(u)/100000000))
391                 else:
392                     print "%s %s" % (addr, str(Decimal(c)/100000000))
393
394     elif cmd in [ 'contacts']:
395         for addr in wallet.addressbook:
396             print addr, "   ", wallet.labels.get(addr)
397
398     elif cmd == 'eval':
399         print eval(args[1])
400         wallet.save()
401
402     elif cmd in [ 'addresses']:
403         for addr in wallet.all_addresses():
404             if options.show_all or not wallet.is_change(addr):
405
406                 flags = wallet.get_address_flags(addr)
407                 label = wallet.labels.get(addr,'')
408                 
409                 if label: label = "\"%s\""%label
410
411                 if options.show_balance:
412                     h = wallet.history.get(addr,[])
413                     #ni = no = 0
414                     #for item in h:
415                     #    if item['is_input']:  ni += 1
416                     #    else:              no += 1
417                     b = format_satoshis(wallet.get_addr_balance(addr)[0])
418                 else: b=''
419                 m_addr = "%34s"%addr
420                 if options.show_keys:
421                     m_addr += ':' + str(wallet.get_private_key_base58(addr, password))
422                 print flags, m_addr, b, label
423
424     if cmd == 'history':
425         lines = wallet.get_tx_history()
426         b = 0 
427         for line in lines:
428             import datetime
429             v = line['value'] 
430             b += v
431             try:
432                 time_str = str( datetime.datetime.fromtimestamp( line['timestamp']))
433             except:
434                 print line['timestamp']
435                 time_str = 'pending'
436             label = line.get('label')
437             if not label: label = line['tx_hash']
438             else: label = label + ' '*(64 - len(label) )
439
440             print time_str , "  " + label + "  " + format_satoshis(v)+ "  "+ format_satoshis(b)
441         print "# balance: ", format_satoshis(b)
442
443     elif cmd == 'label':
444         try:
445             tx = args[1]
446             label = ' '.join(args[2:])
447         except:
448             print_error("Error. Syntax:  label <tx_hash> <text>")
449             sys.exit(1)
450         wallet.labels[tx] = label
451         wallet.save()
452             
453     elif cmd in ['payto', 'mktx']:
454         if from_addr and is_temporary:
455             if from_addr.find(":") == -1:
456                 keypair = from_addr + ":" + prompt_password('Private key:', False)
457             else:
458                 keypair = from_addr
459                 from_addr = keypair.split(':')[0]
460             if not wallet.import_key(keypair,password):
461                 print_error("Error: Invalid key pair")
462                 exit(1)
463             wallet.history[from_addr] = interface.retrieve_history(from_addr)
464             wallet.update_tx_history()
465             change_addr = from_addr
466
467         if options.change_addr:
468             change_addr = options.change_addr
469
470         for k, v in wallet.labels.items():
471             if v == to_address:
472                 to_address = k
473                 print "alias", to_address
474                 break
475             if change_addr and v == change_addr:
476                 change_addr = k
477         try:
478             tx = wallet.mktx( to_address, amount, label, password,
479                 fee = options.tx_fee, change_addr = change_addr, from_addr = from_addr )
480         except:
481             import traceback
482             traceback.print_exc(file=sys.stdout)
483             tx = None
484
485         if tx and cmd=='payto': 
486             r, h = wallet.sendtx( tx )
487             print h
488         else:
489             print tx
490
491         if is_temporary:
492             wallet.imported_keys.pop(from_addr)
493             del(wallet.history[from_addr])
494         wallet.save()
495
496     elif cmd == 'sendtx':
497         tx = args[1]
498         r, h = wallet.sendtx( tx )
499         print h
500
501     elif cmd == 'password':
502         try:
503             seed = wallet.pw_decode( wallet.seed, password)
504         except StandardError:
505             sys.exit("Error: Password does not decrypt this wallet.")
506
507         new_password = prompt_password('New password:')
508         wallet.update_password(seed, password, new_password)
509
510     elif cmd == 'signmessage':
511         if len(args) < 3:
512             print_error("Error: Invalid usage of signmessage.")
513             print known_commands[cmd]
514             sys.exit(1)
515         address = args[1]
516         message = ' '.join(args[2:])
517         if len(args) > 3:
518             print "Warning: Message was reconstructed from several arguments:", repr(message)
519         print wallet.sign_message(address, message, password)
520
521     elif cmd == 'verifymessage':
522         try:
523             address = args[1]
524             signature = args[2]
525             message = ' '.join(args[3:])
526         except:
527             print_error("Error: Not all parameters were given, displaying help instead.")
528             print known_commands[cmd]
529             sys.exit(1)
530         if len(args) > 4:
531             print "Warning: Message was reconstructed from several arguments:", repr(message)
532         try:
533             wallet.verify_message(address, signature, message)
534             print True
535         except:
536             print False
537
538     elif cmd == 'freeze':
539         addr = args[1]
540         print self.wallet.freeze(addr)
541         
542     elif cmd == 'unfreeze':
543         addr = args[1]
544         print self.wallet.unfreeze(addr)
545
546     elif cmd == 'prioritize':
547         addr = args[1]
548         print self.wallet.prioritize(addr)
549
550     elif cmd == 'unprioritize':
551         addr = args[1]
552         print self.wallet.unprioritize(addr)
553