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()