electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

run_electrum (16569B)


      1 #!/usr/bin/env python3
      2 # -*- mode: python -*-
      3 #
      4 # Electrum - lightweight Bitcoin client
      5 # Copyright (C) 2011 thomasv@gitorious
      6 #
      7 # Permission is hereby granted, free of charge, to any person
      8 # obtaining a copy of this software and associated documentation files
      9 # (the "Software"), to deal in the Software without restriction,
     10 # including without limitation the rights to use, copy, modify, merge,
     11 # publish, distribute, sublicense, and/or sell copies of the Software,
     12 # and to permit persons to whom the Software is furnished to do so,
     13 # subject to the following conditions:
     14 #
     15 # The above copyright notice and this permission notice shall be
     16 # included in all copies or substantial portions of the Software.
     17 #
     18 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
     19 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
     20 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
     21 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
     22 # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
     23 # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
     24 # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
     25 # SOFTWARE.
     26 import os
     27 import sys
     28 
     29 
     30 MIN_PYTHON_VERSION = "3.6.1"  # FIXME duplicated from setup.py
     31 _min_python_version_tuple = tuple(map(int, (MIN_PYTHON_VERSION.split("."))))
     32 
     33 
     34 if sys.version_info[:3] < _min_python_version_tuple:
     35     sys.exit("Error: Electrum requires Python version >= %s..." % MIN_PYTHON_VERSION)
     36 
     37 
     38 import warnings
     39 import asyncio
     40 from typing import TYPE_CHECKING, Optional
     41 
     42 
     43 script_dir = os.path.dirname(os.path.realpath(__file__))
     44 is_bundle = getattr(sys, 'frozen', False)
     45 is_local = not is_bundle and os.path.exists(os.path.join(script_dir, "electrum.desktop"))
     46 is_android = 'ANDROID_DATA' in os.environ
     47 
     48 if is_local:  # running from source
     49     # developers should probably see all deprecation warnings.
     50     warnings.simplefilter('default', DeprecationWarning)
     51 
     52 if is_local or is_android:
     53     sys.path.insert(0, os.path.join(script_dir, 'packages'))
     54 
     55 
     56 def check_imports():
     57     # pure-python dependencies need to be imported here for pyinstaller
     58     try:
     59         import dns
     60         import certifi
     61         import qrcode
     62         import google.protobuf
     63         import aiorpcx
     64     except ImportError as e:
     65         sys.exit(f"Error: {str(e)}. Try 'sudo python3 -m pip install <module-name>'")
     66     # the following imports are for pyinstaller
     67     from google.protobuf import descriptor
     68     from google.protobuf import message
     69     from google.protobuf import reflection
     70     from google.protobuf import descriptor_pb2
     71     # make sure that certificates are here
     72     assert os.path.exists(certifi.where())
     73 
     74 
     75 if not is_android:
     76     check_imports()
     77 
     78 
     79 from electrum.logging import get_logger, configure_logging
     80 from electrum import util
     81 from electrum import constants
     82 from electrum import SimpleConfig
     83 from electrum.wallet_db import WalletDB
     84 from electrum.wallet import Wallet
     85 from electrum.storage import WalletStorage
     86 from electrum.util import print_msg, print_stderr, json_encode, json_decode, UserCancelled
     87 from electrum.util import InvalidPassword, BITCOIN_BIP21_URI_SCHEME
     88 from electrum.commands import get_parser, known_commands, Commands, config_variables
     89 from electrum import daemon
     90 from electrum import keystore
     91 from electrum.util import create_and_start_event_loop
     92 
     93 if TYPE_CHECKING:
     94     import threading
     95 
     96     from electrum.plugin import Plugins
     97 
     98 _logger = get_logger(__name__)
     99 
    100 
    101 # get password routine
    102 def prompt_password(prompt, confirm=True):
    103     import getpass
    104     password = getpass.getpass(prompt, stream=None)
    105     if password and confirm:
    106         password2 = getpass.getpass("Confirm: ")
    107         if password != password2:
    108             sys.exit("Error: Passwords do not match.")
    109     if not password:
    110         password = None
    111     return password
    112 
    113 
    114 def init_cmdline(config_options, wallet_path, server, *, config: 'SimpleConfig'):
    115     cmdname = config.get('cmd')
    116     cmd = known_commands[cmdname]
    117 
    118     if cmdname == 'signtransaction' and config.get('privkey'):
    119         cmd.requires_wallet = False
    120         cmd.requires_password = False
    121 
    122     if cmdname in ['payto', 'paytomany'] and config.get('unsigned'):
    123         cmd.requires_password = False
    124 
    125     if cmdname in ['payto', 'paytomany'] and config.get('broadcast'):
    126         cmd.requires_network = True
    127 
    128     # instantiate wallet for command-line
    129     storage = WalletStorage(wallet_path)
    130 
    131     if cmd.requires_wallet and not storage.file_exists():
    132         print_msg("Error: Wallet file not found.")
    133         print_msg("Type 'electrum create' to create a new wallet, or provide a path to a wallet with the -w option")
    134         sys_exit(1)
    135 
    136     # important warning
    137     if cmd.name in ['getprivatekeys']:
    138         print_stderr("WARNING: ALL your private keys are secret.")
    139         print_stderr("Exposing a single private key can compromise your entire wallet!")
    140         print_stderr("In particular, DO NOT use 'redeem private key' services proposed by third parties.")
    141 
    142     # will we need a password
    143     if not storage.is_encrypted():
    144         db = WalletDB(storage.read(), manual_upgrades=False)
    145         use_encryption = db.get('use_encryption')
    146     else:
    147         use_encryption = True
    148 
    149     # commands needing password
    150     if  ( (cmd.requires_wallet and storage.is_encrypted() and server is False)\
    151        or (cmdname == 'load_wallet' and storage.is_encrypted())\
    152        or (cmd.requires_password and use_encryption)):
    153         if storage.is_encrypted_with_hw_device():
    154             # this case is handled later in the control flow
    155             password = None
    156         elif config.get('password'):
    157             password = config.get('password')
    158         else:
    159             password = prompt_password('Password:', False)
    160             if not password:
    161                 print_msg("Error: Password required")
    162                 sys_exit(1)
    163     else:
    164         password = None
    165 
    166     config_options['password'] = config_options.get('password') or password
    167 
    168     if cmd.name == 'password':
    169         new_password = prompt_password('New password:')
    170         config_options['new_password'] = new_password
    171 
    172 
    173 def get_connected_hw_devices(plugins: 'Plugins'):
    174     supported_plugins = plugins.get_hardware_support()
    175     # scan devices
    176     devices = []
    177     devmgr = plugins.device_manager
    178     for splugin in supported_plugins:
    179         name, plugin = splugin.name, splugin.plugin
    180         if not plugin:
    181             e = splugin.exception
    182             _logger.error(f"{name}: error during plugin init: {repr(e)}")
    183             continue
    184         try:
    185             u = devmgr.unpaired_device_infos(None, plugin)
    186         except Exception as e:
    187             _logger.error(f'error getting device infos for {name}: {repr(e)}')
    188             continue
    189         devices += list(map(lambda x: (name, x), u))
    190     return devices
    191 
    192 
    193 def get_password_for_hw_device_encrypted_storage(plugins: 'Plugins') -> str:
    194     devices = get_connected_hw_devices(plugins)
    195     if len(devices) == 0:
    196         print_msg("Error: No connected hw device found. Cannot decrypt this wallet.")
    197         sys.exit(1)
    198     elif len(devices) > 1:
    199         print_msg("Warning: multiple hardware devices detected. "
    200                   "The first one will be used to decrypt the wallet.")
    201     # FIXME we use the "first" device, in case of multiple ones
    202     name, device_info = devices[0]
    203     devmgr = plugins.device_manager
    204     try:
    205         client = devmgr.client_by_id(device_info.device.id_)
    206         return client.get_password_for_storage_encryption()
    207     except UserCancelled:
    208         sys.exit(0)
    209 
    210 
    211 async def run_offline_command(config, config_options, plugins: 'Plugins'):
    212     cmdname = config.get('cmd')
    213     cmd = known_commands[cmdname]
    214     password = config_options.get('password')
    215     if 'wallet_path' in cmd.options and config_options.get('wallet_path') is None:
    216         config_options['wallet_path'] = config.get_wallet_path()
    217     if cmd.requires_wallet:
    218         storage = WalletStorage(config.get_wallet_path())
    219         if storage.is_encrypted():
    220             if storage.is_encrypted_with_hw_device():
    221                 password = get_password_for_hw_device_encrypted_storage(plugins)
    222                 config_options['password'] = password
    223             storage.decrypt(password)
    224         db = WalletDB(storage.read(), manual_upgrades=False)
    225         wallet = Wallet(db, storage, config=config)
    226         config_options['wallet'] = wallet
    227     else:
    228         wallet = None
    229     # check password
    230     if cmd.requires_password and wallet.has_password():
    231         try:
    232             wallet.check_password(password)
    233         except InvalidPassword:
    234             print_msg("Error: This password does not decode this wallet.")
    235             sys.exit(1)
    236     if cmd.requires_network:
    237         print_msg("Warning: running command offline")
    238     # arguments passed to function
    239     args = [config.get(x) for x in cmd.params]
    240     # decode json arguments
    241     if cmdname not in ('setconfig',):
    242         args = list(map(json_decode, args))
    243     # options
    244     kwargs = {}
    245     for x in cmd.options:
    246         kwargs[x] = (config_options.get(x) if x in ['wallet_path', 'wallet', 'password', 'new_password'] else config.get(x))
    247     cmd_runner = Commands(config=config)
    248     func = getattr(cmd_runner, cmd.name)
    249     result = await func(*args, **kwargs)
    250     # save wallet
    251     if wallet:
    252         wallet.save_db()
    253     return result
    254 
    255 
    256 def init_plugins(config, gui_name):
    257     from electrum.plugin import Plugins
    258     return Plugins(config, gui_name)
    259 
    260 
    261 loop = None  # type: Optional[asyncio.AbstractEventLoop]
    262 stop_loop = None  # type: Optional[asyncio.Future]
    263 loop_thread = None  # type: Optional[threading.Thread]
    264 
    265 def sys_exit(i):
    266     # stop event loop and exit
    267     if loop:
    268         loop.call_soon_threadsafe(stop_loop.set_result, 1)
    269         loop_thread.join(timeout=1)
    270     sys.exit(i)
    271 
    272 
    273 def main():
    274     # The hook will only be used in the Qt GUI right now
    275     util.setup_thread_excepthook()
    276     # on macOS, delete Process Serial Number arg generated for apps launched in Finder
    277     sys.argv = list(filter(lambda x: not x.startswith('-psn'), sys.argv))
    278 
    279     # old 'help' syntax
    280     if len(sys.argv) > 1 and sys.argv[1] == 'help':
    281         sys.argv.remove('help')
    282         sys.argv.append('-h')
    283 
    284     # old '-v' syntax
    285     # Due to this workaround that keeps old -v working,
    286     # more advanced usages of -v need to use '-v='.
    287     # e.g. -v=debug,network=warning,interface=error
    288     try:
    289         i = sys.argv.index('-v')
    290     except ValueError:
    291         pass
    292     else:
    293         sys.argv[i] = '-v*'
    294 
    295     # read arguments from stdin pipe and prompt
    296     for i, arg in enumerate(sys.argv):
    297         if arg == '-':
    298             if not sys.stdin.isatty():
    299                 sys.argv[i] = sys.stdin.read()
    300                 break
    301             else:
    302                 raise Exception('Cannot get argument from stdin')
    303         elif arg == '?':
    304             sys.argv[i] = input("Enter argument:")
    305         elif arg == ':':
    306             sys.argv[i] = prompt_password('Enter argument (will not echo):', False)
    307 
    308     # parse command line
    309     parser = get_parser()
    310     args = parser.parse_args()
    311 
    312     # config is an object passed to the various constructors (wallet, interface, gui)
    313     if is_android:
    314         from jnius import autoclass
    315         build_config = autoclass("org.electrum.electrum.BuildConfig")
    316         config_options = {
    317             'verbosity': '*' if build_config.DEBUG else '',
    318             'cmd': 'gui',
    319             'gui': 'kivy',
    320             'single_password':True,
    321         }
    322     else:
    323         config_options = args.__dict__
    324         f = lambda key: config_options[key] is not None and key not in config_variables.get(args.cmd, {}).keys()
    325         config_options = {key: config_options[key] for key in filter(f, config_options.keys())}
    326         if config_options.get('server'):
    327             config_options['auto_connect'] = False
    328 
    329     config_options['cwd'] = os.getcwd()
    330 
    331     # fixme: this can probably be achieved with a runtime hook (pyinstaller)
    332     if is_bundle and os.path.exists(os.path.join(sys._MEIPASS, 'is_portable')):
    333         config_options['portable'] = True
    334 
    335     if config_options.get('portable'):
    336         config_options['electrum_path'] = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'electrum_data')
    337 
    338     if not config_options.get('verbosity'):
    339         warnings.simplefilter('ignore', DeprecationWarning)
    340 
    341     # check uri
    342     uri = config_options.get('url')
    343     if uri:
    344         if not uri.lower().startswith(BITCOIN_BIP21_URI_SCHEME + ':'):
    345             print_stderr('unknown command:', uri)
    346             sys.exit(1)
    347 
    348     config = SimpleConfig(config_options)
    349 
    350     if config.get('testnet'):
    351         constants.set_testnet()
    352     elif config.get('regtest'):
    353         constants.set_regtest()
    354     elif config.get('simnet'):
    355         constants.set_simnet()
    356 
    357     cmdname = config.get('cmd')
    358 
    359     if cmdname == 'daemon' and config.get("detach"):
    360         # fork before creating the asyncio event loop
    361         pid = os.fork()
    362         if pid:
    363             print_stderr("starting daemon (PID %d)" % pid)
    364             sys.exit(0)
    365         else:
    366             # redirect standard file descriptors
    367             sys.stdout.flush()
    368             sys.stderr.flush()
    369             si = open(os.devnull, 'r')
    370             so = open(os.devnull, 'w')
    371             se = open(os.devnull, 'w')
    372             os.dup2(si.fileno(), sys.stdin.fileno())
    373             os.dup2(so.fileno(), sys.stdout.fileno())
    374             os.dup2(se.fileno(), sys.stderr.fileno())
    375 
    376     global loop, stop_loop, loop_thread
    377     loop, stop_loop, loop_thread = create_and_start_event_loop()
    378 
    379     try:
    380         handle_cmd(
    381             cmdname=cmdname,
    382             config=config,
    383             config_options=config_options,
    384         )
    385     except Exception:
    386         _logger.exception("")
    387         sys_exit(1)
    388 
    389 
    390 def handle_cmd(*, cmdname: str, config: 'SimpleConfig', config_options: dict):
    391     if cmdname == 'gui':
    392         configure_logging(config)
    393         fd = daemon.get_file_descriptor(config)
    394         if fd is not None:
    395             plugins = init_plugins(config, config.get('gui', 'qt'))
    396             d = daemon.Daemon(config, fd)
    397             try:
    398                 d.run_gui(config, plugins)
    399             except BaseException as e:
    400                 _logger.exception('daemon.run_gui errored')
    401                 sys_exit(1)
    402             else:
    403                 sys_exit(0)
    404         else:
    405             result = daemon.request(config, 'gui', (config_options,))
    406 
    407     elif cmdname == 'daemon':
    408 
    409         configure_logging(config)
    410         fd = daemon.get_file_descriptor(config)
    411         if fd is not None:
    412             # run daemon
    413             init_plugins(config, 'cmdline')
    414             d = daemon.Daemon(config, fd)
    415             d.run_daemon()
    416             sys_exit(0)
    417         else:
    418             # FIXME this message is lost in detached mode (parent process already exited after forking)
    419             print_msg("Daemon already running")
    420             sys_exit(1)
    421     else:
    422         # command line
    423         cmd = known_commands[cmdname]
    424         wallet_path = config.get_wallet_path()
    425         if not config.get('offline'):
    426             init_cmdline(config_options, wallet_path, True, config=config)
    427             timeout = config.get('timeout', 60)
    428             if timeout: timeout = int(timeout)
    429             try:
    430                 result = daemon.request(config, 'run_cmdline', (config_options,), timeout)
    431             except daemon.DaemonNotRunning:
    432                 print_msg("Daemon not running; try 'electrum daemon -d'")
    433                 if not cmd.requires_network:
    434                     print_msg("To run this command without a daemon, use --offline")
    435                 sys_exit(1)
    436             except Exception as e:
    437                 print_stderr(str(e) or repr(e))
    438                 sys_exit(1)
    439         else:
    440             if cmd.requires_network:
    441                 print_msg("This command cannot be run offline")
    442                 sys_exit(1)
    443             init_cmdline(config_options, wallet_path, False, config=config)
    444             plugins = init_plugins(config, 'cmdline')
    445             coro = run_offline_command(config, config_options, plugins)
    446             fut = asyncio.run_coroutine_threadsafe(coro, loop)
    447             try:
    448                 result = fut.result()
    449             except Exception as e:
    450                 print_stderr(str(e) or repr(e))
    451                 sys_exit(1)
    452     if isinstance(result, str):
    453         print_msg(result)
    454     elif type(result) is dict and result.get('error'):
    455         print_stderr(result.get('error'))
    456         sys_exit(1)
    457     elif result is not None:
    458         print_msg(json_encode(result))
    459     sys_exit(0)
    460 
    461 
    462 if __name__ == '__main__':
    463     main()