3 # Electrum - lightweight Bitcoin client
4 # Copyright (C) 2011 thomasv@gitorious
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.
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.
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/>.
26 sys.exit("Error: python-ecdsa does not seem to be installed. Try 'sudo pip install ecdsa'")
31 sys.exit("Error: AES does not seem to be installed. Try 'sudo pip install slowaes'")
36 from electrum import *
38 from decimal import Decimal
41 'help':'Prints this help',
42 'validateaddress':'Check that the address is valid',
43 'balance': "Display the balance of your wallet or of an address.\nSyntax: balance [<address>]",
44 'contacts': "Show your list of contacts",
45 'create':'Create a wallet',
46 'restore':'Restore a wallet',
47 'payto':"""Create and broadcast a transaction.
48 Syntax: payto <recipient> <amount> [label]
49 <recipient> can be a bitcoin address or a label
50 options:\n --fee, -f: set transaction fee\n --fromaddr, -s: send from address -\n --changeaddr, -c: send change to address
53 'Broadcasts a transaction to the network. \nSyntax: sendtx <tx>\n<tx> must be in hexadecimal.',
55 "Changes your password",
57 """Shows your list of addresses.
59 -a: show all addresses, including change addresses
61 -b: show the balance of addresses""",
63 'history':"Shows the transaction history",
64 'label':'Assign a label to an item\nSyntax: label <tx_hash> <label>',
66 """Create a signed transaction, password protected.
67 Syntax: mktx <recipient> <amount> [label]
68 options:\n --fee, -f: set transaction fee\n --fromaddr, -s: send from address -\n --changeaddr, -c: send change to address
70 'signtx':"Sign an unsigned transaction created by a deseeded wallet\nSyntax: signtx <filename>",
72 "Print the generation seed of your wallet.",
74 'Import a private key\nSyntax: importprivkey <privatekey>',
76 '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 "',
78 '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 "',
80 "Run python eval() on an object\nSyntax: eval <expression>\nExample: eval \"wallet.aliases\"",
82 "Get config parameter.",
84 "Set config parameter.",
86 "Remove seed from the wallet. The seed is stored in a file that has the name of the wallet plus '.seed'",
88 "Restore seed of the wallet. The wallet must have no seed, and the seed must match the wallet's master public key.",
97 offline_commands = [ 'password', 'mktx', 'signtx',
99 'help', 'validateaddress',
100 'signmessage', 'verifymessage',
101 'eval', 'set', 'get', 'create', 'addresses',
102 'importprivkey', 'seed',
105 'prioritize','unprioritize']
108 protected_commands = ['payto', 'password', 'mktx', 'signtx', 'seed', 'importprivkey','signmessage' ]
110 # get password routine
111 def prompt_password(prompt, confirm=True):
113 if sys.stdin.isatty():
114 password = getpass.getpass(prompt)
115 if password and confirm:
116 password2 = getpass.getpass("Confirm: ")
117 if password != password2:
118 sys.exit("Error: Passwords do not match.")
120 password = raw_input(prompt)
126 usage = "usage: %prog [options] command\nCommands: "+ (', '.join(known_commands))
127 parser = optparse.OptionParser(prog=usage)
128 parser.add_option("-g", "--gui", dest="gui", help="User interface: qt, lite, gtk or text")
129 parser.add_option("-w", "--wallet", dest="wallet_path", help="wallet path (default: electrum.dat)")
130 parser.add_option("-o", "--offline", action="store_true", dest="offline", default=False, help="remain offline")
131 parser.add_option("-a", "--all", action="store_true", dest="show_all", default=False, help="show all addresses")
132 parser.add_option("-b", "--balance", action="store_true", dest="show_balance", default=False, help="show the balance at listed addresses")
133 parser.add_option("-k", "--keys",action="store_true", dest="show_keys",default=False, help="show the private keys of listed addresses")
134 parser.add_option("-f", "--fee", dest="tx_fee", default="0.005", help="set tx fee")
135 parser.add_option("-F", "--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.")
136 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")
137 parser.add_option("-s", "--server", dest="server", default=None, help="set server host:port:protocol, where protocol is t or h")
138 parser.add_option("-p", "--proxy", dest="proxy", default=None, help="set proxy [type:]host[:port], where type is socks4,socks5 or http")
139 parser.add_option("-v", "--verbose", action="store_true", dest="verbose", default=False, help="show debugging information")
140 parser.add_option("-P", "--portable", action="store_true", dest="portable", default=False, help="portable wallet")
141 parser.add_option("-L", "--lang", dest="language", default=None, help="defaut language used in GUI")
142 parser.add_option("-u", "--usb", dest="bitkey", action="store_true", help="Turn on support for hardware wallets (EXPERIMENTAL)")
146 if __name__ == '__main__':
148 parser = arg_parser()
149 options, args = parser.parse_args()
150 set_verbosity(options.verbose)
152 # config is an object passed to the various constructors (wallet, interface, gui)
153 if 'ANDROID_DATA' in os.environ:
154 config_options = {'wallet_path':"/sdcard/electrum.dat", 'portable':True, 'verbose':True, 'gui':'android'}
156 config_options = eval(str(options))
157 for k, v in config_options.items():
158 if v is None: config_options.pop(k)
160 config = SimpleConfig(config_options)
161 wallet = Wallet(config)
166 elif len(args)==1 and re.match('^bitcoin:', args[0]):
171 firstarg = args[1] if len(args) > 1 else ''
173 #this entire if/else block is just concerned with importing the
174 #right GUI toolkit based the GUI command line option given
176 pref_gui = config.get('gui','classic')
177 if pref_gui == 'gtk':
179 import lib.gui as gui
181 import electrum.gui as gui
182 elif pref_gui in ['classic', 'qt']:
184 import lib.gui_qt as gui
186 import electrum.gui_qt as gui
187 elif pref_gui == 'lite':
189 import lib.gui_lite as gui
191 import electrum.gui_lite as gui
192 elif pref_gui == 'text':
194 import lib.gui_text as gui
196 import electrum.gui_text as gui
197 elif pref_gui == 'android':
199 import lib.gui_android as gui
201 import electrum.gui_android as gui
203 sys.exit("Error: Unknown GUI: " + pref_gui )
206 interface = Interface(config, True)
207 wallet.interface = interface
209 if interface.is_connected:
210 interface.send([('server.peers.subscribe',[])])
212 set_language(config.get('language'))
213 gui = gui.ElectrumGui(wallet, config)
215 found = config.wallet_file_exists
217 a = gui.restore_or_create()
220 s = gui.network_dialog()
223 wallet.new_seed(None)
224 wallet.init_mpk( wallet.seed )
226 # ask for seed and gap.
227 sg = gui.seed_dialog()
231 wallet.gap_limit = gap
234 wallet.master_public_key = seed
236 wallet.seed = str(seed)
237 wallet.init_mpk( wallet.seed )
240 # generate the first addresses, in case we are offline
241 if s is None or a == 'create':
247 verifier = WalletVerifier(interface, config)
248 wallet.set_verifier(verifier)
249 synchronizer = WalletSynchronizer(wallet, config)
252 if not found and a == 'restore' and s is not None:
254 keep_it = gui.restore_wallet()
255 wallet.fill_addressbook()
258 traceback.print_exc(file=sys.stdout)
261 if not keep_it: exit()
264 gui.password_dialog()
275 # we use daemon threads, their termination is enforced.
276 # this sleep command gives them time to terminate cleanly.
280 if cmd not in known_commands:
283 if not config.wallet_file_exists and cmd not in ['help','create','restore']:
284 print_msg("Error: Wallet file not found.")
285 print_msg("Type 'electrum create' to create a new wallet, or provide a path to a wallet with the -w option")
288 if cmd in ['create', 'restore']:
289 if config.wallet_file_exists:
290 sys.exit("Error: Remove the existing wallet first!")
291 password = prompt_password("Password (hit return if you do not wish to encrypt your wallet):")
293 server = config.get('server')
294 if not server: server = pick_random_server()
295 w_host, w_port, w_protocol = server.split(':')
296 host = raw_input("server (default:%s):"%w_host)
297 port = raw_input("port (default:%s):"%w_port)
298 protocol = raw_input("protocol [t=tcp;h=http;n=native] (default:%s):"%w_protocol)
299 fee = raw_input("fee (default:%s):"%( str(Decimal(wallet.fee)/100000000)) )
300 gap = raw_input("gap limit (default 5):")
301 if host: w_host = host
302 if port: w_port = port
303 if protocol: w_protocol = protocol
304 wallet.config.set_key('server', w_host + ':' + w_port + ':' +w_protocol)
305 if fee: wallet.fee = float(fee)
306 if gap: wallet.gap_limit = int(gap)
309 seed = raw_input("seed:")
313 print_error("Warning: Not hex, trying decode.")
314 seed = mnemonic_decode( seed.split(' ') )
316 sys.exit("Error: No seed")
320 wallet.master_public_key = seed
322 wallet.seed = str(seed)
323 wallet.init_mpk( wallet.seed )
325 if not options.offline:
326 interface = Interface(config)
328 wallet.interface = interface
330 verifier = WalletVerifier(interface, config)
331 wallet.set_verifier(verifier)
333 print_msg("Recovering wallet...")
334 WalletSynchronizer(wallet, config).start()
336 if wallet.is_found():
337 print_msg("Recovery successful")
339 print_msg("Warning: Found no history for this wallet")
342 wallet.fill_addressbook()
344 print_msg("Wallet saved in '%s'"%wallet.config.path)
346 wallet.new_seed(None)
347 wallet.init_mpk( wallet.seed )
348 wallet.synchronize() # there is no wallet thread
350 print_msg("Your wallet generation seed is: " + wallet.seed)
351 print_msg("Please keep it in a safe place; if you lose it, you will not be able to restore your wallet.")
352 print_msg("Equivalently, your wallet seed can be stored and recovered with the following mnemonic code:")
353 print_msg("\""+' '.join(mnemonic_encode(wallet.seed))+"\"")
354 print_msg("Wallet saved in '%s'"%wallet.config.path)
357 wallet.update_password(wallet.seed, None, password)
360 if cmd in ['payto', 'mktx']:
363 amount = int( 100000000 * Decimal(args[2]) )
365 label = ' '.join(args[3:])
367 options.tx_fee = int( 100000000 * Decimal(options.tx_fee) )
373 if cmd not in offline_commands and not options.offline:
374 interface = Interface(config)
375 interface.register_callback('connected', lambda: sys.stderr.write("Connected to " + interface.connection_msg + "\n"))
377 wallet.interface = interface
378 synchronizer = WalletSynchronizer(wallet, config)
383 # check if --from_addr not in wallet (for mktx/payto)
386 if options.from_addr:
387 from_addr = options.from_addr
388 if from_addr not in wallet.all_addresses():
392 if cmd=='addresses' and options.show_keys:
393 print_msg("WARNING: ALL your private keys are secret.")
394 print_msg("Exposing a single private key can compromise your entire wallet!")
395 print_msg("In particular, DO NOT use 'redeem private key' services proposed by third parties.")
397 # commands needing password
398 if cmd in protected_commands or ( cmd=='addresses' and options.show_keys):
399 if wallet.use_encryption and not is_temporary:
400 password = prompt_password('Password:', False)
402 print_msg("Error: Password required")
406 seed = wallet.decode_seed(password)
408 print_msg("Error: This password does not decode this wallet.")
414 if cmd == 'importprivkey':
415 # See if they specificed a key on the cmd line, if not prompt
419 sec = prompt_password('Enter PrivateKey (will not echo):', False)
421 addr = wallet.import_key(sec,password)
423 print_msg("Keypair imported: ", addr)
424 except BaseException as e:
425 print_msg("Error: Keypair import failed: " + str(e))
429 if cmd2 not in known_commands:
431 print_msg("Type 'electrum help <command>' to see the help for a specific command")
432 print_msg("Type 'electrum --help' to see the list of options")
433 print_msg("List of commands:", ', '.join(known_commands))
435 print_msg(known_commands[cmd2])
438 print_msg(seed + ' "' + ' '.join(mnemonic_encode(seed)) + '"')
440 elif cmd == 'deseed':
442 print_msg("Error: This wallet has no seed")
443 elif wallet.use_encryption:
444 print_msg("Error: This wallet is encrypted")
446 ns = wallet.config.path + '.seed'
447 print_msg("Warning: you are going to extract the seed from '%s'\nThe seed will be saved in '%s'"%(wallet.config.path,ns))
448 if raw_input("Are you sure you want to continue? (y/n) ") in ['y','Y','yes']:
450 f.write(repr({'seed':wallet.seed, 'imported_keys':wallet.imported_keys})+"\n")
453 for k in wallet.imported_keys.keys(): wallet.imported_keys[k] = ''
457 print_msg("Action canceled.")
459 elif cmd == 'reseed':
461 print_msg("Warning: This wallet already has a seed", wallet.seed)
463 ns = wallet.config.path + '.seed'
469 sys.exit("Error: Seed file not found")
472 d = ast.literal_eval( data )
474 imported_keys = d.get('imported_keys',{})
476 sys.exit("Error: Error with seed file")
478 mpk = wallet.master_public_key
480 wallet.imported_keys = imported_keys
481 wallet.use_encryption = False
482 wallet.init_mpk(seed)
483 if mpk == wallet.master_public_key:
485 print_msg("Done: " + wallet.config.path)
487 print_msg("Error: Master public key does not match")
489 elif cmd == 'validateaddress':
491 print_msg(wallet.is_valid(addr))
493 elif cmd == 'balance':
499 c, u = wallet.get_balance()
501 print_msg(Decimal( c ) / 100000000 , Decimal( u ) / 100000000)
503 print_msg(Decimal( c ) / 100000000)
506 c, u = wallet.get_addr_balance(addr)
508 print_msg("%s %s, %s" % (addr, str(Decimal(c)/100000000), str(Decimal(u)/100000000)))
510 print_msg("%s %s" % (addr, str(Decimal(c)/100000000)))
512 elif cmd in [ 'contacts']:
513 for addr in wallet.addressbook:
514 print_msg(addr, " ", wallet.labels.get(addr))
517 print_msg(eval(args[1]))
522 print_msg(wallet.config.get(key))
525 key, value = args[1:3]
526 if key not in ['seed', 'seed_version', 'master_public_key', 'use_encryption']:
527 wallet.config.set_key(key, value, True)
532 elif cmd in [ 'addresses']:
533 for addr in wallet.all_addresses():
534 if options.show_all or not wallet.is_change(addr):
536 flags = wallet.get_address_flags(addr)
537 label = wallet.labels.get(addr,'')
539 if label: label = "\"%s\""%label
541 if options.show_balance:
542 h = wallet.history.get(addr,[])
545 # if item['is_input']: ni += 1
547 b = format_satoshis(wallet.get_addr_balance(addr)[0])
550 if options.show_keys:
551 m_addr += ':' + str(wallet.get_private_key_base58(addr, password))
552 print_msg(flags, m_addr, b, label)
556 for item in wallet.get_tx_history():
557 tx_hash, conf, is_mine, value, fee, balance, timestamp = item
559 time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
563 label, is_default_label = wallet.get_label(tx_hash)
564 if not label: label = tx_hash
565 else: label = label + ' '*(64 - len(label) )
567 print_msg("%17s"%time_str, " " + label + " " + format_satoshis(value)+ " "+ format_satoshis(balance))
568 print_msg("# balance: ", format_satoshis(balance))
573 label = ' '.join(args[2:])
575 print_msg("Error. Syntax: label <tx_hash> <text>")
577 wallet.labels[tx] = label
580 elif cmd in ['payto', 'mktx']:
581 if from_addr and is_temporary:
582 if from_addr.find(":") == -1:
583 keypair = from_addr + ":" + prompt_password('Private key:', False)
586 from_addr = keypair.split(':')[0]
587 if not wallet.import_key(keypair,password):
588 print_msg("Error: Invalid key pair")
590 wallet.history[from_addr] = interface.retrieve_history(from_addr)
591 wallet.update_tx_history()
592 change_addr = from_addr
594 if options.change_addr:
595 change_addr = options.change_addr
597 for k, v in wallet.labels.items():
600 print_msg("alias", to_address)
602 if change_addr and v == change_addr:
605 tx = wallet.mktx( [(to_address, amount)], label, password,
606 fee = options.tx_fee, change_addr = change_addr, from_addr = from_addr )
609 traceback.print_exc(file=sys.stdout)
612 if tx and cmd=='payto':
613 r, h = wallet.sendtx( tx )
619 wallet.imported_keys.pop(from_addr)
620 del(wallet.history[from_addr])
623 elif cmd == 'signtx':
626 f = open(filename, 'r')
627 d = ast.literal_eval(f.read())
630 outputs = d['outputs']
631 tx = wallet.signed_tx( inputs, outputs, password )
634 elif cmd == 'sendtx':
636 r, h = wallet.sendtx( tx )
639 elif cmd == 'password':
640 new_password = prompt_password('New password:')
641 wallet.update_password(seed, password, new_password)
643 elif cmd == 'signmessage':
645 print_msg("Error: Invalid usage of signmessage.")
646 print_msg(known_commands[cmd])
649 message = ' '.join(args[2:])
651 print_msg("Warning: Message was reconstructed from several arguments:", repr(message))
652 print_msg(wallet.sign_message(address, message, password))
654 elif cmd == 'verifymessage':
658 message = ' '.join(args[3:])
660 print_msg("Error: Not all parameters were given, displaying help instead.")
661 print_msg(known_commands[cmd])
664 print_msg("Warning: Message was reconstructed from several arguments:", repr(message))
666 wallet.verify_message(address, signature, message)
668 except BaseException as e:
669 print_error("Verification error: {0}".format(e))
672 elif cmd == 'freeze':
674 print_msg(wallet.freeze(addr))
676 elif cmd == 'unfreeze':
678 print_msg(wallet.unfreeze(addr))
680 elif cmd == 'prioritize':
682 print_msg(wallet.prioritize(addr))
684 elif cmd == 'unprioritize':
686 print_msg(wallet.unprioritize(addr))
689 if cmd not in offline_commands and not options.offline: