diff --git a/README.rst b/README.rst index 44e47fc4..00cf37e6 100644 --- a/README.rst +++ b/README.rst @@ -47,7 +47,7 @@ Using Homebrew:: protoc --proto_path=lib/ --python_out=lib/ lib/paymentrequest.proto # Run - ./electrum-zcl + ./electrum For Linux: @@ -87,7 +87,7 @@ Create translations (optional):: Run:: - ./electrum-zcl + ./electrum For Linux with docker: diff --git a/electrum b/electrum new file mode 100755 index 00000000..898f0420 --- /dev/null +++ b/electrum @@ -0,0 +1,442 @@ +#!/usr/bin/env python3 +# -*- mode: python -*- +# +# Electrum - lightweight Bitcoin client +# Copyright (C) 2011 thomasv@gitorious +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +import os +import sys + +# from https://gist.github.com/tito/09c42fb4767721dc323d +import threading +try: + import jnius +except: + jnius = None +if jnius: + orig_thread_run = threading.Thread.run + def thread_check_run(*args, **kwargs): + try: + return orig_thread_run(*args, **kwargs) + finally: + jnius.detach() + threading.Thread.run = thread_check_run + +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, "electrum.desktop")) +is_android = 'ANDROID_DATA' in os.environ +is_macOS = sys.platform == 'darwin' + +# move this back to gui/kivy/__init.py once plugins are moved +os.environ['KIVY_DATA_DIR'] = os.path.abspath(os.path.dirname(__file__)) + '/gui/kivy/data/' + +if is_local or is_android: + sys.path.insert(0, os.path.join(script_dir, 'packages')) + + +def check_imports(): + # pure-python dependencies need to be imported here for pyinstaller + try: + import dns + import pyaes + import ecdsa + import requests + import qrcode + import pbkdf2 + import google.protobuf + import jsonrpclib + except ImportError as e: + sys.exit("Error: %s. Try 'sudo pip install '"%str(e)) + # 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 + from jsonrpclib import SimpleJSONRPCServer + # make sure that certificates are here + if is_bundle and is_macOS: + requests.utils.DEFAULT_CA_BUNDLE_PATH = os.path.join(os.path.dirname(__file__), 'cacert.pem') + assert os.path.exists(requests.utils.DEFAULT_CA_BUNDLE_PATH) + + +if not is_android: + check_imports() + +# load local module as electrum +if is_local or is_android or is_macOS: + import imp + imp.load_module('electrum', *imp.find_module('lib')) + imp.load_module('electrum_gui', *imp.find_module('gui')) + imp.load_module('electrum_plugins', *imp.find_module('plugins')) + + +from electrum import bitcoin +from electrum import SimpleConfig, Network +from electrum.wallet import Wallet, Imported_Wallet +from electrum.storage import WalletStorage +from electrum.util import print_msg, print_stderr, json_encode, json_decode +from electrum.util import set_verbosity, InvalidPassword +from electrum.commands import get_parser, known_commands, Commands, config_variables +from electrum import daemon +from electrum import keystore +from electrum.mnemonic import Mnemonic +import electrum_plugins + +# get password routine +def prompt_password(prompt, confirm=True): + import getpass + password = getpass.getpass(prompt, stream=None) + 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 run_non_RPC(config): + cmdname = config.get('cmd') + + storage = WalletStorage(config.get_wallet_path()) + if storage.file_exists(): + sys.exit("Error: Remove the existing wallet first!") + + def password_dialog(): + return prompt_password("Password (hit return if you do not wish to encrypt your wallet):") + + if cmdname == 'restore': + text = config.get('text').strip() + passphrase = config.get('passphrase', '') + password = password_dialog() if keystore.is_private(text) else None + if keystore.is_address_list(text): + wallet = Imported_Wallet(storage) + for x in text.split(): + wallet.import_address(x) + elif keystore.is_private_key_list(text): + k = keystore.Imported_KeyStore({}) + storage.put('keystore', k.dump()) + storage.put('use_encryption', bool(password)) + wallet = Imported_Wallet(storage) + for x in text.split(): + wallet.import_private_key(x, password) + storage.write() + else: + if keystore.is_seed(text): + k = keystore.from_seed(text, passphrase, False) + elif keystore.is_master_key(text): + k = keystore.from_master_key(text) + else: + sys.exit("Error: Seed or key not recognized") + if password: + k.update_password(None, password) + storage.put('keystore', k.dump()) + storage.put('wallet_type', 'standard') + storage.put('use_encryption', bool(password)) + storage.write() + wallet = Wallet(storage) + if not config.get('offline'): + network = Network(config) + network.start() + wallet.start_threads(network) + print_msg("Recovering wallet...") + wallet.synchronize() + wallet.wait_until_synchronized() + msg = "Recovery successful" if wallet.is_found() else "Found no history for this wallet" + else: + msg = "This wallet was restored offline. It may contain more addresses than displayed." + print_msg(msg) + + elif cmdname == 'create': + password = password_dialog() + passphrase = config.get('passphrase', '') + seed_type = 'segwit' if config.get('segwit') else 'standard' + seed = Mnemonic('en').make_seed(seed_type) + k = keystore.from_seed(seed, passphrase, False) + storage.put('keystore', k.dump()) + storage.put('wallet_type', 'standard') + wallet = Wallet(storage) + wallet.update_password(None, password, True) + 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.") + + wallet.storage.write() + print_msg("Wallet saved in '%s'" % wallet.storage.path) + sys.exit(0) + + +def init_daemon(config_options): + config = SimpleConfig(config_options) + storage = WalletStorage(config.get_wallet_path()) + if 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) + if storage.is_encrypted(): + 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) + else: + password = None + config_options['password'] = password + + +def init_cmdline(config_options, server): + config = SimpleConfig(config_options) + 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 + + # instanciate wallet for command-line + storage = WalletStorage(config.get_wallet_path()) + + if 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) + + # 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_wallet and storage.is_encrypted() and server is None)\ + or (cmd.requires_password and (storage.get('use_encryption') or storage.is_encrypted())): + 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) + else: + password = None + + config_options['password'] = password + + if cmd.name == 'password': + new_password = prompt_password('New password:') + config_options['new_password'] = new_password + + return cmd, password + + +def run_offline_command(config, config_options): + cmdname = config.get('cmd') + cmd = known_commands[cmdname] + password = config_options.get('password') + if cmd.requires_wallet: + storage = WalletStorage(config.get_wallet_path()) + if storage.is_encrypted(): + storage.decrypt(password) + wallet = Wallet(storage) + else: + wallet = None + # check password + if cmd.requires_password and storage.get('use_encryption'): + try: + seed = wallet.check_password(password) + except InvalidPassword: + print_msg("Error: This password does not decode this wallet.") + sys.exit(1) + if cmd.requires_network: + print_msg("Warning: running command offline") + # arguments passed to function + args = [config.get(x) for x in cmd.params] + # decode json arguments + args = list(map(json_decode, args)) + # options + kwargs = {} + for x in cmd.options: + kwargs[x] = (config_options.get(x) if x in ['password', 'new_password'] else config.get(x)) + cmd_runner = Commands(config, wallet, None) + func = getattr(cmd_runner, cmd.name) + result = func(*args, **kwargs) + # save wallet + if wallet: + wallet.storage.write() + return result + +def init_plugins(config, gui_name): + from electrum.plugins import Plugins + return Plugins(config, is_local or is_android, gui_name) + +if __name__ == '__main__': + + # on osx, delete Process Serial Number arg generated for apps launched in Finder + sys.argv = list(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] = input("Enter argument:") + 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, + 'cmd': 'gui', + 'gui': 'kivy', + } + else: + config_options = args.__dict__ + f = lambda key: config_options[key] is not None and key not in config_variables.get(args.cmd, {}).keys() + config_options = {key: config_options[key] for key in filter(f, config_options.keys())} + if config_options.get('server'): + config_options['auto_connect'] = False + + config_options['cwd'] = os.getcwd() + + # fixme: this can probably be achieved with a runtime hook (pyinstaller) + if is_bundle and hasattr(sys, '_MEIPASS') and os.path.exists(os.path.join(sys._MEIPASS, 'is_portable')): + config_options['portable'] = True + + if config_options.get('portable'): + config_options['electrum_path'] = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'electrum_data') + + # kivy sometimes freezes when we write to sys.stderr + set_verbosity(config_options.get('verbose') and config_options.get('gui')!='kivy') + + # check uri + uri = config_options.get('url') + if uri: + if not uri.startswith('bitcoin:'): + print_stderr('unknown command:', uri) + sys.exit(1) + config_options['url'] = uri + + # todo: defer this to gui + config = SimpleConfig(config_options) + cmdname = config.get('cmd') + + if config.get('testnet'): + bitcoin.NetworkConstants.set_testnet() + + # run non-RPC commands separately + if cmdname in ['create', 'restore']: + run_non_RPC(config) + sys.exit(0) + + if cmdname == 'gui': + fd, server = daemon.get_fd_or_server(config) + if fd is not None: + plugins = init_plugins(config, config.get('gui', 'qt')) + d = daemon.Daemon(config, fd, True) + d.start() + d.init_gui(config, plugins) + sys.exit(0) + else: + result = server.gui(config_options) + + elif cmdname == 'daemon': + subcommand = config.get('subcommand') + if subcommand in ['load_wallet']: + init_daemon(config_options) + + if subcommand in [None, 'start']: + fd, server = daemon.get_fd_or_server(config) + if fd is not None: + if subcommand == 'start': + pid = os.fork() + if pid: + print_stderr("starting daemon (PID %d)" % pid) + sys.exit(0) + init_plugins(config, 'cmdline') + d = daemon.Daemon(config, fd, False) + d.start() + if config.get('websocket_server'): + from electrum import websockets + websockets.WebSocketServer(config, d.network).start() + if config.get('requests_dir'): + path = os.path.join(config.get('requests_dir'), 'index.html') + if not os.path.exists(path): + print("Requests directory not configured.") + print("You can configure it using https://github.com/spesmilo/electrum-merchant") + sys.exit(1) + d.join() + sys.exit(0) + else: + result = server.daemon(config_options) + else: + server = daemon.get_server(config) + if server is not None: + result = server.daemon(config_options) + else: + print_msg("Daemon not running") + sys.exit(1) + else: + # command line + server = daemon.get_server(config) + init_cmdline(config_options, server) + if server is not None: + result = server.run_cmdline(config_options) + else: + cmd = known_commands[cmdname] + if cmd.requires_network: + print_msg("Daemon not running; try 'electrum daemon start'") + sys.exit(1) + else: + init_plugins(config, 'cmdline') + result = run_offline_command(config, config_options) + # print result + if isinstance(result, str): + print_msg(result) + elif type(result) is dict and result.get('error'): + print_stderr(result.get('error')) + elif result is not None: + print_msg(json_encode(result)) + sys.exit(0) diff --git a/electrum-env b/electrum-env index 1dffdcd0..42220eda 100755 --- a/electrum-env +++ b/electrum-env @@ -19,6 +19,6 @@ fi export PYTHONPATH="/usr/local/lib/python3.5/site-packages:$PYTHONPATH" -./electrum-zcl "$@" +./electrum "$@" deactivate diff --git a/setup.py b/setup.py index 2051ed27..af9cbf66 100755 --- a/setup.py +++ b/setup.py @@ -54,30 +54,30 @@ setup( install_requires=install_requires, tests_require=tests_requires, packages=[ - 'electrum-zcl', - 'electrum-zcl_gui', - 'electrum-zcl_gui.qt', - 'electrum-zcl_plugins', - 'electrum-zcl_plugins.audio_modem', - 'electrum-zcl_plugins.cosigner_pool', - 'electrum-zcl_plugins.email_requests', - 'electrum-zcl_plugins.greenaddress_instant', - 'electrum-zcl_plugins.hw_wallet', - 'electrum-zcl_plugins.keepkey', - 'electrum-zcl_plugins.labels', - 'electrum-zcl_plugins.ledger', - 'electrum-zcl_plugins.trezor', - 'electrum-zcl_plugins.digitalbitbox', - 'electrum-zcl_plugins.trustedcoin', - 'electrum-zcl_plugins.virtualkeyboard', + 'electrum', + 'electrum_gui', + 'electrum_gui.qt', + 'electrum_plugins', + 'electrum_plugins.audio_modem', + 'electrum_plugins.cosigner_pool', + 'electrum_plugins.email_requests', + 'electrum_plugins.greenaddress_instant', + 'electrum_plugins.hw_wallet', + 'electrum_plugins.keepkey', + 'electrum_plugins.labels', + 'electrum_plugins.ledger', + 'electrum_plugins.trezor', + 'electrum_plugins.digitalbitbox', + 'electrum_plugins.trustedcoin', + 'electrum_plugins.virtualkeyboard', ], package_dir={ - 'electrum-zcl': 'lib', - 'electrum-zcl_gui': 'gui', - 'electrum-zcl_plugins': 'plugins', + 'electrum': 'lib', + 'electrum_gui': 'gui', + 'electrum_plugins': 'plugins', }, package_data={ - 'electrum-zcl': [ + 'electrum': [ 'servers.json', 'servers_testnet.json', 'currencies.json', @@ -85,10 +85,10 @@ setup( 'checkpoints_testnet.json', 'www/index.html', 'wordlist/*.txt', - 'locale/*/LC_MESSAGES/electrum-zcl.mo', + 'locale/*/LC_MESSAGES/electrum.mo', ] }, - scripts=['electrum-zcl'], + scripts=['electrum'], data_files=data_files, description="Lightweight Zclassic Wallet", author="BTCP Community",