#!/usr/bin/env python2 # -*- mode: python -*- # # Electrum - lightweight Bitcoin client # Copyright (C) 2011 thomasv@gitorious # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from decimal import Decimal import json import os import re import sys import time import traceback import threading import socket import Queue from collections import defaultdict DAEMON_SOCKET = 'daemon.sock' script_dir = os.path.dirname(os.path.realpath(__file__)) is_bundle = getattr(sys, 'frozen', False) is_local = not is_bundle and os.path.exists(os.path.join(script_dir, "setup-release.py")) is_android = 'ANDROID_DATA' in os.environ if is_local or is_android: sys.path.insert(0, os.path.join(script_dir, 'packages')) elif is_bundle and sys.platform=='darwin': sys.path.insert(0, os.getcwd() + "/lib/python2.7/packages") # pure-python dependencies need to be imported here for pyinstaller try: import dns import aes import ecdsa import requests import six import qrcode import pbkdf2 import google.protobuf except ImportError as e: sys.exit("Error: %s. Try 'sudo pip install '"%e.message) # the following imports are for pyinstaller from google.protobuf import descriptor from google.protobuf import message from google.protobuf import reflection from google.protobuf import descriptor_pb2 # check that we have the correct version of ecdsa try: from ecdsa.ecdsa import curve_secp256k1, generator_secp256k1 except Exception: sys.exit("cannot import ecdsa.curve_secp256k1. You probably need to upgrade ecdsa.\nTry: sudo pip install --upgrade ecdsa") # load local module as electrum if is_bundle or is_local or is_android: import imp imp.load_module('electrum', *imp.find_module('lib')) imp.load_module('electrum_gui', *imp.find_module('gui')) from electrum import util from electrum import SimpleConfig, Network, Wallet, WalletStorage from electrum.util import print_msg, print_error, print_stderr, print_json, set_verbosity, InvalidPassword from electrum.plugins import Plugins, run_hook, always_hook from electrum.commands import get_parser, known_commands, Commands, config_variables # get password routine def prompt_password(prompt, confirm=True): import getpass password = getpass.getpass(prompt) if password and confirm: password2 = getpass.getpass("Confirm: ") if password != password2: sys.exit("Error: Passwords do not match.") if not password: password = None return password def init_gui(config, network, plugins): gui_name = config.get('gui', 'qt') if gui_name in ['lite', 'classic']: gui_name = 'qt' gui = __import__('electrum_gui.' + gui_name, fromlist=['electrum_gui']) gui = gui.ElectrumGui(config, network, plugins) return gui def init_cmdline(config): cmdname = config.get('cmd') cmd = known_commands[cmdname] if cmdname == 'signtransaction' and config.get('privkey'): cmd.requires_wallet = False cmd.requires_password = False if cmdname in ['payto', 'paytomany'] and config.get('unsigned'): cmd.requires_password = False if cmdname in ['payto', 'paytomany'] and config.get('broadcast'): cmd.requires_network = True if cmdname in ['createrawtx'] and config.get('unsigned'): cmd.requires_password = False cmd.requires_wallet = False if cmdname == 'listrequests' and config.get('status'): cmd.requires_network = True # instanciate wallet for command-line storage = WalletStorage(config.get_wallet_path()) if cmd.name in ['create', 'restore']: if storage.file_exists: sys.exit("Error: Remove the existing wallet first!") if config.get('password') is not None: password = config.get('password') elif cmd.name == 'restore' and config.get('mpk'): password = None else: password = prompt_password("Password (hit return if you do not wish to encrypt your wallet):") if cmd.name == 'restore': mpk = config.get('mpk') if mpk: if Wallet.is_old_mpk(mpk): wallet = Wallet.from_old_mpk(mpk, storage) if Wallet.is_xpub(mpk): wallet = Wallet.from_xpub(mpk, storage) else: import getpass seed = getpass.getpass(prompt="seed:", stream=None) if config.get('concealed') else raw_input("seed:") if not Wallet.is_seed(seed): sys.exit("Error: Invalid seed") wallet = Wallet.from_seed(seed, password, storage) if not config.get('offline'): network = Network(config) network.start() wallet.start_threads(network) print_msg("Recovering wallet...") wallet.restore(lambda x: x) if wallet.is_found(): print_msg("Recovery successful") else: print_msg("Warning: Found no history for this wallet") else: wallet.synchronize() print_msg("Warning: This wallet was restored offline. It may contain more addresses than displayed.") else: wallet = Wallet(storage) seed = wallet.make_seed() wallet.add_seed(seed, password) wallet.create_master_keys(password) wallet.create_main_account(password) wallet.synchronize() print_msg("Your wallet generation seed is:\n\"%s\"" % seed) print_msg("Please keep it in a safe place; if you lose it, you will not be able to restore your wallet.") print_msg("Wallet saved in '%s'" % wallet.storage.path) # terminate sys.exit(0) if cmd.name not in ['create', 'restore'] and cmd.requires_wallet and not storage.file_exists: print_msg("Error: Wallet file not found.") print_msg("Type 'electrum create' to create a new wallet, or provide a path to a wallet with the -w option") sys.exit(0) # create wallet instance wallet = Wallet(storage) if cmd.requires_wallet else None # notify plugins always_hook('cmdline_load_wallet', wallet) # important warning if cmd.name in ['getprivatekeys']: print_stderr("WARNING: ALL your private keys are secret.") print_stderr("Exposing a single private key can compromise your entire wallet!") print_stderr("In particular, DO NOT use 'redeem private key' services proposed by third parties.") # commands needing password if cmd.requires_password and wallet.use_encryption: if config.get('password'): password = config.get('password') else: password = prompt_password('Password:', False) if not password: print_msg("Error: Password required") sys.exit(1) # check password try: seed = wallet.check_password(password) except InvalidPassword: print_msg("Error: This password does not decode this wallet.") sys.exit(1) else: password = None # run the command if cmd.name == 'deseed': if not wallet.seed: print_msg("Error: This wallet has no seed") else: ns = wallet.storage.path + '.seedless' print_msg("Warning: you are going to create a seedless wallet'\nIt will be saved in '%s'" % ns) if raw_input("Are you sure you want to continue? (y/n) ") in ['y', 'Y', 'yes']: wallet.storage.path = ns wallet.seed = '' wallet.storage.put('seed', '', True) wallet.use_encryption = False wallet.storage.put('use_encryption', wallet.use_encryption, True) for k in wallet.imported_keys.keys(): wallet.imported_keys[k] = '' wallet.storage.put('imported_keys', wallet.imported_keys, True) print_msg("Done.") else: print_msg("Action canceled.") sys.exit(0) elif cmd.name == 'password': new_password = prompt_password('New password:') wallet.update_password(password, new_password) sys.exit(0) return cmd, password def run_command(config, network, password): cmdname = config.get('cmd') cmd = known_commands[cmdname] # instanciate wallet for command-line storage = WalletStorage(config.get_wallet_path()) # create wallet instance wallet = Wallet(storage) if cmd.requires_wallet else None # arguments passed to function args = map(lambda x: config.get(x), cmd.params) # decode json arguments def json_decode(x): try: return json.loads(x) except: return x args = map(json_decode, args) # options args += map(lambda x: config.get(x), cmd.options) cmd_runner = Commands(config, wallet, network) cmd_runner.password = password func = getattr(cmd_runner, cmd.name) result = func(*args) if wallet: wallet.stop_threads() return result class ClientThread(util.DaemonThread): def __init__(self, server, s): util.DaemonThread.__init__(self) self.server = server self.client_pipe = util.SocketPipe(s) self.response_queue = Queue.Queue() self.server.add_client(self) self.subscriptions = defaultdict(list) self.network = self.server.network def run(self): config_options = self.client_pipe.get() password = config_options.get('password') config = SimpleConfig(config_options) cmd = config.get('cmd') if cmd == 'gui': if self.server.gui: if hasattr(server.gui, 'new_window'): path = config.get_wallet_path() self.server.gui.new_window(path, config.get('url')) response = "ok" else: response = "error: current GUI does not support multiple windows" else: response = "Error: Electrum is running in daemon mode. Please stop the daemon first." elif cmd == 'daemon': sub = config.get('subcommand') assert sub in ['start', 'stop', 'status'] if sub == 'start': response = "Daemon already running" elif sub == 'status': p = self.network.get_parameters() response = { 'path': self.network.config.path, 'server': p[0], 'blockchain_height': self.network.get_local_height(), 'server_height': self.network.get_server_height(), 'nodes': self.network.get_interfaces(), 'connected': self.network.is_connected(), 'auto_connect': p[4], } elif sub == 'stop': self.server.stop() response = "Daemon stopped" else: try: response = run_command(config, self.network, password) except BaseException as e: err = traceback.format_exc() response = {'error':err} # send response and exit self.client_pipe.send(response) self.server.remove_client(self) class NetworkServer(util.DaemonThread): def __init__(self, config, network): util.DaemonThread.__init__(self) self.debug = False self.config = config self.pipe = util.QueuePipe() self.network = network self.lock = threading.RLock() # each GUI is a client of the daemon self.clients = [] # gui is None is we run as daemon self.gui = None def add_client(self, client): for key in ['fee', 'status', 'banner', 'updated', 'servers', 'interfaces']: value = self.network.get_status_value(key) client.response_queue.put({'method':'network.status', 'params':[key, value]}) with self.lock: self.clients.append(client) print_error("new client:", len(self.clients)) def remove_client(self, client): with self.lock: self.clients.remove(client) print_error("client quit:", len(self.clients)) def run(self): s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) daemon_socket = os.path.join(self.config.path, DAEMON_SOCKET) if os.path.exists(daemon_socket): os.remove(daemon_socket) daemon_timeout = self.config.get('daemon_timeout', None) s.bind(daemon_socket) s.listen(5) s.settimeout(0.1) while self.is_running(): try: connection, address = s.accept() except socket.timeout: continue client = ClientThread(self, connection) client.start() print_error("Daemon exiting") def get_daemon(config, start_daemon): daemon_socket = os.path.join(config.path, DAEMON_SOCKET) daemon_started = False while True: try: s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) s.connect(daemon_socket) return s except socket.error: if not start_daemon: return False elif not daemon_started: daemon_started = True else: time.sleep(0.1) except: # do not use daemon if AF_UNIX is not available (windows) return False def check_www_dir(rdir): # rewrite index.html every time import urllib, urlparse, shutil, os if not os.path.exists(rdir): os.mkdir(rdir) index = os.path.join(rdir, 'index.html') src = os.path.join(os.path.dirname(__file__), 'www', 'index.html') shutil.copy(src, index) files = [ "https://code.jquery.com/jquery-1.9.1.min.js", "https://raw.githubusercontent.com/davidshimjs/qrcodejs/master/qrcode.js", "https://code.jquery.com/ui/1.10.3/jquery-ui.js", "https://code.jquery.com/ui/1.10.3/themes/smoothness/jquery-ui.css" ] for URL in files: path = urlparse.urlsplit(URL).path filename = os.path.basename(path) path = os.path.join(rdir, filename) if not os.path.exists(path): print_error("downloading ", URL) urllib.urlretrieve(URL, path) if __name__ == '__main__': # make sure that certificates are here assert os.path.exists(requests.utils.DEFAULT_CA_BUNDLE_PATH) # on osx, delete Process Serial Number arg generated for apps launched in Finder sys.argv = filter(lambda x: not x.startswith('-psn'), sys.argv) # old 'help' syntax if len(sys.argv)>1 and sys.argv[1] == 'help': sys.argv.remove('help') sys.argv.append('-h') # read arguments from stdin pipe and prompt for i, arg in enumerate(sys.argv): if arg == '-': if not sys.stdin.isatty(): sys.argv[i] = sys.stdin.read() break else: raise BaseException('Cannot get argument from stdin') elif arg == '?': sys.argv[i] = prompt_password('Enter argument (will not echo):', False) # parse command line parser = get_parser() args = parser.parse_args() # config is an object passed to the various constructors (wallet, interface, gui) if is_android: config_options = { 'verbose': True, 'gui': 'android', 'auto_connect': True, } else: config_options = args.__dict__ for k, v in config_options.items(): if v is None or (k in config_variables.get(args.cmd, {}).keys()): config_options.pop(k) if config_options.get('server'): config_options['auto_connect'] = False if config_options.get('portable'): config_options['electrum_path'] = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'electrum_data') set_verbosity(config_options.get('verbose')) # check uri uri = config_options.get('url') if uri: if os.path.exists(uri): # assume this is a payment request uri = "bitcoin:?r=file://"+ os.path.join(os.getcwd(), uri) if not re.match('^bitcoin:', uri): print_stderr('unknown command:', uri) sys.exit(1) config_options['url'] = uri config = SimpleConfig(config_options) cmd_name = config.get('cmd') # initialize plugins. plugins = None if not is_android: gui_name = config.get('gui', 'qt') if cmd_name == 'gui' else 'cmdline' plugins = Plugins(config, is_bundle or is_local or is_android, gui_name) # get password if needed if cmd_name not in ['gui', 'daemon']: cmd, password = init_cmdline(config) if not cmd.requires_network or config.get('offline'): result = run_command(config, None, password) print_json(result) sys.exit(1) # check if daemon is running s = get_daemon(config, False) if s: p = util.SocketPipe(s) p.set_timeout(1000000) p.send(config_options) result = p.get() s.close() if type(result) in [str, unicode]: print_msg(result) elif result is not None: if type(result) is dir and result.get('error'): print_stderr(result.get('error')) else: print_json(result) sys.exit(0) # daemon is not running if cmd_name == 'gui': network = Network(config) network.start() server = NetworkServer(config, network) server.start() server.gui = init_gui(config, network, plugins) server.gui.main() elif cmd_name == 'daemon': subcommand = config.get('subcommand') if subcommand in ['status', 'stop']: print_msg("Daemon not running") sys.exit(1) elif subcommand == 'start': p = os.fork() if p == 0: network = Network(config) network.start() server = NetworkServer(config, network) if config.get('websocket_server'): import websockets websockets.WebSocketServer(config, server).start() if config.get('requests_dir'): check_www_dir(config.get('requests_dir')) server.start() server.join() else: print_stderr("starting daemon (PID %d)"%p) sys.exit(0) else: print_msg("syntax: electrum daemon ") sys.exit(1) else: print_msg("Network daemon is not running. Try 'electrum daemon start'\nIf you want to run this command offline, use the -o flag.") sys.exit(1)