trun_electrum - electrum - Electrum Bitcoin wallet
 (HTM) git clone https://git.parazyd.org/electrum
 (DIR) Log
 (DIR) Files
 (DIR) Refs
 (DIR) Submodules
       ---
       trun_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()