2017-02-05 14:31:17 +03:00
#!/usr/bin/env python3
2015-02-21 12:24:40 +01:00
# -*- mode: python -*-
2011-11-04 18:00:37 +01:00
#
# Electrum - lightweight Bitcoin client
# Copyright (C) 2011 thomasv@gitorious
#
2016-02-23 11:36:42 +01:00
# 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:
2011-11-04 18:00:37 +01:00
#
2016-02-23 11:36:42 +01:00
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
2011-11-04 18:00:37 +01:00
#
2016-02-23 11:36:42 +01:00
# 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.
2013-11-12 19:19:32 -08:00
import os
import sys
2020-02-28 19:47:56 +01:00
2019-02-15 22:06:10 +01:00
2025-01-10 13:16:04 +00:00
MIN_PYTHON_VERSION = " 3.10.0 " # FIXME duplicated from setup.py
2019-02-15 22:06:10 +01:00
_min_python_version_tuple = tuple ( map ( int , ( MIN_PYTHON_VERSION . split ( " . " ) ) ) )
if sys . version_info [ : 3 ] < _min_python_version_tuple :
sys . exit ( " Error: Electrum requires Python version >= %s ... " % MIN_PYTHON_VERSION )
2020-02-28 19:47:56 +01:00
import warnings
import asyncio
2025-03-15 11:44:24 +01:00
from typing import TYPE_CHECKING , Optional , Dict
2020-02-28 19:47:56 +01:00
2015-02-24 18:41:29 +01:00
script_dir = os . path . dirname ( os . path . realpath ( __file__ ) )
2022-03-23 19:20:22 +01:00
is_pyinstaller = getattr ( sys , ' frozen ' , False )
2013-03-12 13:48:16 +01:00
is_android = ' ANDROID_DATA ' in os . environ
2022-03-23 19:20:22 +01:00
is_appimage = ' APPIMAGE ' in os . environ
2023-03-31 16:00:19 +00:00
is_binary_distributable = is_pyinstaller or is_android or is_appimage
2022-03-23 19:20:22 +01:00
# is_local: unpacked tar.gz but not pip installed, or git clone
2023-03-31 16:00:19 +00:00
is_local = ( not is_binary_distributable
2022-03-23 19:20:22 +01:00
and os . path . exists ( os . path . join ( script_dir , " electrum.desktop " ) ) )
is_git_clone = is_local and os . path . exists ( os . path . join ( script_dir , " .git " ) )
2013-03-12 13:48:16 +01:00
2022-03-23 19:20:22 +01:00
if is_git_clone :
2024-10-30 11:35:17 +01:00
# developers should probably see all deprecation warnings unless explicitly overruled
if not any ( [ ' DeprecationWarning ' in x for x in sys . warnoptions ] ) :
warnings . simplefilter ( ' default ' , DeprecationWarning )
2019-05-28 21:38:27 +02:00
2015-02-24 18:41:29 +01:00
if is_local or is_android :
sys . path . insert ( 0 , os . path . join ( script_dir , ' packages ' ) )
2015-01-27 13:50:02 +01:00
2022-05-29 03:26:20 +02:00
if is_pyinstaller :
# Keep an open file handle for the binary that started us. On Windows, this
# prevents users from moving or renaming the exe file while running (doing which
# causes ImportErrors and other runtime failures). (see #4072)
_file = open ( sys . executable , ' rb ' )
2016-01-28 14:38:10 +01:00
2025-06-15 18:25:42 +00:00
# when running from source, on Windows, also search for DLLs in inner 'electrum' folder
if is_local and os . name == ' nt ' : # fixme: duplicated between main script and __init__.py :(
os . add_dll_directory ( os . path . join ( os . path . dirname ( __file__ ) , ' electrum ' ) )
2016-01-28 14:38:10 +01:00
def check_imports ( ) :
2016-01-27 20:21:20 +05:30
# pure-python dependencies need to be imported here for pyinstaller
try :
import dns
2018-12-13 23:11:59 +01:00
import certifi
2016-01-27 20:21:20 +05:30
import qrcode
import google . protobuf
2018-07-27 12:29:04 +02:00
import aiorpcx
2025-06-15 15:17:59 +02:00
import aiohttp
import aiohttp_socks
import electrum_ecc
import jsonpatch
import electrum_aionostr
2016-01-27 20:21:20 +05:30
except ImportError as e :
2025-06-15 15:17:59 +02:00
sys . exit ( f " Error: { str ( e ) } . Some dependencies are missing. Have you read the README? Or just try ' $ python3 -m pip install -r contrib/requirements/requirements.txt ' " )
2025-02-13 18:48:11 +00:00
if not ( ( 0 , 25 , 0 ) < = aiorpcx . _version < ( 0 , 26 ) ) :
raise RuntimeError ( f ' aiorpcX version { aiorpcx . _version } does not match required: 0.25.0<=ver<0.26 ' )
2016-01-27 20:21:20 +05:30
# 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
2016-01-28 14:38:10 +01:00
# make sure that certificates are here
2018-12-13 23:11:59 +01:00
assert os . path . exists ( certifi . where ( ) )
2016-01-28 14:38:10 +01:00
2015-01-26 14:14:16 +01:00
2016-01-28 14:38:10 +01:00
if not is_android :
check_imports ( )
2015-01-26 14:14:16 +01:00
2017-03-28 15:49:50 +02:00
2025-11-30 05:57:03 +00:00
if is_android :
# hack to make pycryptodomex work on Android
# from https://github.com/kivy/python-for-android/issues/1866#issuecomment-927157780
import ctypes
ctypes . pythonapi = ctypes . PyDLL ( " libpython %d . %d .so " % sys . version_info [ : 2 ] ) # replaces ctypes.PyDLL(None)
2021-04-14 07:01:23 +02:00
sys . _ELECTRUM_RUNNING_VIA_RUNELECTRUM = True # used by logging.py
from electrum . logging import get_logger , configure_logging # import logging submodule first
2018-10-10 20:29:51 +02:00
from electrum import util
2023-03-19 13:32:43 +01:00
from electrum . payment_identifier import PaymentIdentifier
2018-10-10 20:29:51 +02:00
from electrum import SimpleConfig
2020-02-05 15:13:37 +01:00
from electrum . wallet_db import WalletDB
2018-10-10 20:29:51 +02:00
from electrum . wallet import Wallet
2020-02-28 19:47:56 +01:00
from electrum . storage import WalletStorage
2018-03-14 12:42:42 +01:00
from electrum . util import print_msg , print_stderr , json_encode , json_decode , UserCancelled
2021-11-23 14:42:43 +01:00
from electrum . util import InvalidPassword
2025-03-06 11:43:50 +01:00
from electrum . plugin import Plugins
2025-03-16 15:00:24 +01:00
from electrum . commands import get_parser , get_simple_parser , known_commands , Commands , config_variables
2016-02-01 10:20:22 +01:00
from electrum import daemon
2024-02-12 19:02:02 +00:00
from electrum . util import create_and_start_event_loop , UserFacingException , JsonRPCError
2021-04-01 04:36:02 +02:00
from electrum . i18n import set_language
2019-04-26 18:52:26 +02:00
2020-02-28 19:47:56 +01:00
if TYPE_CHECKING :
2020-10-05 17:07:33 +02:00
import threading
2019-04-26 18:52:26 +02:00
_logger = get_logger ( __name__ )
2019-03-14 14:56:22 -07:00
2012-06-06 19:26:05 +02:00
2012-10-12 01:50:54 +02:00
# get password routine
2023-09-08 15:46:06 +00:00
def prompt_password ( prompt : str , * , confirm : bool = True ) - > Optional [ str ] :
2012-10-12 01:50:54 +02:00
import getpass
2015-10-28 09:33:35 +01:00
password = getpass . getpass ( prompt , stream = None )
2015-08-16 16:11:52 +02:00
if password and confirm :
password2 = getpass . getpass ( " Confirm: " )
if password != password2 :
sys . exit ( " Error: Passwords do not match. " )
2012-10-12 01:50:54 +02:00
if not password :
password = None
return password
2013-11-12 19:19:32 -08:00
2023-09-08 15:46:06 +00:00
def init_cmdline ( config_options , wallet_path , * , rpcserver : bool , config : ' SimpleConfig ' ) :
2015-05-28 15:22:30 +02:00
cmdname = config . get ( ' cmd ' )
cmd = known_commands [ cmdname ]
2011-11-04 18:00:37 +01:00
2015-06-10 23:21:25 +02:00
if cmdname in [ ' payto ' , ' paytomany ' ] and config . get ( ' unsigned ' ) :
2015-05-31 17:38:57 +02:00
cmd . requires_password = False
2015-06-10 23:21:25 +02:00
if cmdname in [ ' payto ' , ' paytomany ' ] and config . get ( ' broadcast ' ) :
cmd . requires_network = True
2025-05-31 11:45:57 +02:00
if cmd . requires_wallet and not wallet_path :
print_msg ( " wallet path not provided. " )
sys_exit ( 1 )
2018-02-10 19:18:48 +01:00
# instantiate wallet for command-line
2025-07-15 13:14:38 +00:00
storage = WalletStorage ( wallet_path , allow_partial_writes = config . WALLET_PARTIAL_WRITES ) if wallet_path else None
2013-08-22 12:39:41 +02:00
2017-03-06 08:33:35 +01:00
if cmd . requires_wallet and not storage . file_exists ( ) :
2015-10-28 11:13:45 +01:00
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 " )
2019-08-31 08:32:06 +02:00
sys_exit ( 1 )
2015-10-28 11:13:45 +01:00
2012-10-02 13:15:10 +02:00
# important warning
2015-08-16 16:30:55 +02:00
if cmd . name in [ ' getprivatekeys ' ] :
2014-04-30 15:27:50 +02:00
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. " )
2012-10-02 13:15:10 +02:00
2011-11-14 20:35:54 +01:00
# commands needing password
2023-09-08 15:46:06 +00:00
if ( ( cmd . requires_wallet and storage . is_encrypted ( ) and not rpcserver )
or ( cmdname == ' load_wallet ' and storage . is_encrypted ( ) )
2023-09-13 18:10:23 +02:00
or ( cmdname in [ ' password ' , ' unlock ' ] )
or ( cmd . requires_password and not rpcserver ) ) :
2017-12-07 11:35:10 +01:00
if storage . is_encrypted_with_hw_device ( ) :
2018-02-10 19:18:48 +01:00
# this case is handled later in the control flow
password = None
2023-08-17 07:52:40 +02:00
elif config . get ( ' password ' ) is not None :
2015-08-16 16:11:52 +02:00
password = config . get ( ' password ' )
2023-08-17 07:52:40 +02:00
if password == ' ' :
password = None
2015-08-16 16:11:52 +02:00
else :
2023-09-08 15:46:06 +00:00
password = prompt_password ( ' Password: ' , confirm = False )
2013-02-26 13:56:48 +01:00
else :
password = None
2018-10-10 20:29:51 +02:00
config_options [ ' password ' ] = config_options . get ( ' password ' ) or password
2012-05-13 01:32:28 +02:00
2021-09-10 18:19:06 -05:00
if cmd . name == ' password ' and ' new_password ' not in config_options :
2012-07-06 21:45:57 -07:00
new_password = prompt_password ( ' New password: ' )
2015-12-23 10:54:31 +01:00
config_options [ ' new_password ' ] = new_password
2015-08-26 17:44:19 +02:00
2020-02-28 19:47:56 +01:00
def get_connected_hw_devices ( plugins : ' Plugins ' ) :
2018-10-31 17:58:47 +01:00
supported_plugins = plugins . get_hardware_support ( )
2018-02-10 19:18:48 +01:00
# scan devices
devices = [ ]
devmgr = plugins . device_manager
2018-10-31 17:58:47 +01:00
for splugin in supported_plugins :
name , plugin = splugin . name , splugin . plugin
if not plugin :
e = splugin . exception
2019-04-26 18:52:26 +02:00
_logger . error ( f " { name } : error during plugin init: { repr ( e ) } " )
2018-10-31 17:58:47 +01:00
continue
2018-02-10 19:18:48 +01:00
try :
2022-12-19 13:08:27 +00:00
u = devmgr . list_pairable_device_infos ( handler = None , plugin = plugin )
2019-04-26 18:52:26 +02:00
except Exception as e :
_logger . error ( f ' error getting device infos for { name } : { repr ( e ) } ' )
2018-02-10 19:18:48 +01:00
continue
devices + = list ( map ( lambda x : ( name , x ) , u ) )
return devices
2020-02-28 19:47:56 +01:00
def get_password_for_hw_device_encrypted_storage ( plugins : ' Plugins ' ) - > str :
2018-02-10 19:18:48 +01:00
devices = get_connected_hw_devices ( plugins )
if len ( devices ) == 0 :
2025-08-26 12:02:33 +02:00
raise UserFacingException ( " Error: No connected hw device found. Cannot decrypt this wallet. " )
2018-02-10 19:18:48 +01:00
elif len ( devices ) > 1 :
print_msg ( " Warning: multiple hardware devices detected. "
" The first one will be used to decrypt the wallet. " )
# FIXME we use the "first" device, in case of multiple ones
name , device_info = devices [ 0 ]
2020-02-28 19:47:56 +01:00
devmgr = plugins . device_manager
2018-03-14 12:42:42 +01:00
try :
2020-02-28 19:47:56 +01:00
client = devmgr . client_by_id ( device_info . device . id_ )
2024-10-10 21:25:34 +00:00
client . handler = client . plugin . create_handler ( None )
2020-02-28 19:47:56 +01:00
return client . get_password_for_storage_encryption ( )
2018-03-14 12:42:42 +01:00
except UserCancelled :
2025-08-26 12:02:33 +02:00
raise
2018-02-10 19:18:48 +01:00
2025-07-15 12:25:47 +00:00
async def run_offline_command ( config : ' SimpleConfig ' , config_options : dict , wallet_path : str , plugins : ' Plugins ' ) :
2015-12-23 10:54:31 +01:00
cmdname = config . get ( ' cmd ' )
cmd = known_commands [ cmdname ]
2017-02-09 17:08:27 +01:00
password = config_options . get ( ' password ' )
2019-09-06 11:06:08 +02:00
if ' wallet_path ' in cmd . options and config_options . get ( ' wallet_path ' ) is None :
2025-05-31 11:45:57 +02:00
config_options [ ' wallet_path ' ] = wallet_path
2017-03-03 16:05:13 +01:00
if cmd . requires_wallet :
2025-07-15 13:14:38 +00:00
storage = WalletStorage ( wallet_path , allow_partial_writes = config . WALLET_PARTIAL_WRITES )
2017-03-06 08:33:35 +01:00
if storage . is_encrypted ( ) :
2017-12-07 11:35:10 +01:00
if storage . is_encrypted_with_hw_device ( ) :
2018-02-10 19:18:48 +01:00
password = get_password_for_hw_device_encrypted_storage ( plugins )
config_options [ ' password ' ] = password
2017-03-06 08:33:35 +01:00
storage . decrypt ( password )
2023-09-22 11:49:53 +02:00
db = WalletDB ( storage . read ( ) , storage = storage , upgrade = True )
2023-06-23 17:36:34 +02:00
wallet = Wallet ( db , config = config )
2019-09-05 17:57:51 +02:00
config_options [ ' wallet ' ] = wallet
2017-03-03 16:05:13 +01:00
else :
wallet = None
2015-12-23 10:54:31 +01:00
# check password
2017-12-07 11:35:10 +01:00
if cmd . requires_password and wallet . has_password ( ) :
2015-12-23 10:54:31 +01:00
try :
2018-10-10 20:29:51 +02:00
wallet . check_password ( password )
2015-12-23 10:54:31 +01:00
except InvalidPassword :
print_msg ( " Error: This password does not decode this wallet. " )
2025-08-26 12:02:33 +02:00
raise
2015-12-23 10:54:31 +01:00
if cmd . requires_network :
2017-02-09 17:08:27 +01:00
print_msg ( " Warning: running command offline " )
2015-08-26 17:44:19 +02:00
# arguments passed to function
2017-01-22 21:25:24 +03:00
args = [ config . get ( x ) for x in cmd . params ]
2015-08-30 17:46:51 +02:00
# decode json arguments
2018-01-13 22:43:57 +01:00
if cmdname not in ( ' setconfig ' , ) :
args = list ( map ( json_decode , args ) )
2015-08-26 17:44:19 +02:00
# options
2017-10-09 11:54:17 +02:00
kwargs = { }
for x in cmd . options :
2019-09-06 11:06:08 +02:00
kwargs [ x ] = ( config_options . get ( x ) if x in [ ' wallet_path ' , ' wallet ' , ' password ' , ' new_password ' ] else config . get ( x ) )
2019-09-05 18:30:04 +02:00
cmd_runner = Commands ( config = config )
2015-08-26 17:44:19 +02:00
func = getattr ( cmd_runner , cmd . name )
2019-08-15 13:17:16 +02:00
result = await func ( * args , * * kwargs )
2015-12-23 10:54:31 +01:00
# save wallet
2015-12-23 15:23:33 +01:00
if wallet :
2020-02-05 15:13:37 +01:00
wallet . save_db ( )
2015-08-26 17:44:19 +02:00
return result
2018-10-10 20:29:51 +02:00
2020-10-05 17:07:33 +02:00
loop = None # type: Optional[asyncio.AbstractEventLoop]
stop_loop = None # type: Optional[asyncio.Future]
loop_thread = None # type: Optional[threading.Thread]
2025-06-03 11:26:23 +02:00
2019-08-15 13:17:16 +02:00
def sys_exit ( i ) :
# stop event loop and exit
2020-10-05 17:07:33 +02:00
if loop :
loop . call_soon_threadsafe ( stop_loop . set_result , 1 )
loop_thread . join ( timeout = 1 )
2019-08-15 13:17:16 +02:00
sys . exit ( i )
2015-05-28 15:22:30 +02:00
2025-06-03 11:26:23 +02:00
2025-04-07 15:01:57 +02:00
def read_config ( config_options : dict ) - > SimpleConfig :
"""
Reads the config file and returns SimpleConfig, on failure it will potentially
show a GUI error dialog if a gui is available, and then re-raise the exception.
"""
try :
return SimpleConfig ( config_options )
except Exception as config_error :
# parse full cmd to find out which UI is being used
full_config_options = parse_command_line ( simple_parser = False )
if full_config_options . get ( " cmd " ) == ' gui ' :
gui_name = full_config_options . get ( SimpleConfig . GUI_NAME . key ( ) , ' qt ' )
try :
gui = __import__ ( f ' electrum.gui. { gui_name } ' , fromlist = [ ' electrum ' ] )
gui . standalone_exception_dialog ( config_error ) # type: ignore
except Exception as e :
print_stderr ( f " Error showing standalone gui dialog: { e } " )
raise
2025-06-03 11:26:23 +02:00
2025-03-16 15:00:24 +01:00
def parse_command_line ( simple_parser = False ) - > Dict :
2025-03-15 11:44:24 +01:00
# parse command line from sys.argv
2025-03-16 15:00:24 +01:00
if simple_parser :
parser = get_simple_parser ( )
options , args = parser . parse_args ( )
config_options = options . __dict__
config_options [ ' cmd ' ] = ' gui '
else :
parser = get_parser ( )
args = parser . parse_args ( )
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 ( SimpleConfig . NETWORK_SERVER . key ( ) ) :
config_options [ SimpleConfig . NETWORK_AUTO_CONNECT . key ( ) ] = False
2025-11-27 16:08:13 +01:00
if config_options . get ( SimpleConfig . NETWORK_PROXY . key ( ) ) :
config_options [ SimpleConfig . NETWORK_PROXY_ENABLED . key ( ) ] = True
2025-03-15 11:44:24 +01:00
config_options [ ' cwd ' ] = cwd = os . getcwd ( )
# fixme: this can probably be achieved with a runtime hook (pyinstaller)
if is_pyinstaller and os . path . exists ( os . path . join ( sys . _MEIPASS , ' is_portable ' ) ) :
config_options [ ' portable ' ] = True
if config_options . get ( ' portable ' ) :
if is_local :
# running from git clone or local source: put datadir next to main script
datadir = os . path . join ( os . path . dirname ( os . path . realpath ( __file__ ) ) , ' electrum_data ' )
else :
# Running a binary or installed source. The most generic but still reasonable thing
# is to use the current working directory. (see #7732)
# note: The main script is often unpacked to a temporary directory from a bundled executable,
# and we don't want to put the datadir inside a temp dir.
# note: Re the portable .exe on Windows, when the user double-clicks it, CWD gets set
# to the parent dir, i.e. we will put the datadir next to the exe.
datadir = os . path . join ( os . path . realpath ( cwd ) , ' electrum_data ' )
config_options [ ' electrum_path ' ] = datadir
if not config_options . get ( ' verbosity ' ) :
warnings . simplefilter ( ' ignore ' , DeprecationWarning )
return config_options
2020-10-05 17:07:33 +02:00
2025-06-03 11:26:23 +02:00
2020-10-05 17:07:33 +02:00
def main ( ) :
2023-08-03 17:02:40 +00:00
global loop , stop_loop , loop_thread
2017-03-28 15:49:50 +02:00
# The hook will only be used in the Qt GUI right now
util . setup_thread_excepthook ( )
2018-04-15 20:45:30 +03:00
# on macOS, delete Process Serial Number arg generated for apps launched in Finder
2017-01-22 21:25:24 +03:00
sys . argv = list ( filter ( lambda x : not x . startswith ( ' -psn ' ) , sys . argv ) )
2015-05-28 15:22:30 +02:00
# old 'help' syntax
2017-01-22 21:25:24 +03:00
if len ( sys . argv ) > 1 and sys . argv [ 1 ] == ' help ' :
2015-05-28 15:22:30 +02:00
sys . argv . remove ( ' help ' )
sys . argv . append ( ' -h ' )
2018-08-30 18:34:04 +02:00
# old '-v' syntax
2019-04-26 20:45:23 +02:00
# Due to this workaround that keeps old -v working,
# more advanced usages of -v need to use '-v='.
# e.g. -v=debug,network=warning,interface=error
2018-08-30 18:34:04 +02:00
try :
i = sys . argv . index ( ' -v ' )
except ValueError :
pass
else :
sys . argv [ i ] = ' -v* '
2015-08-17 09:46:50 +02:00
# read arguments from stdin pipe and prompt
for i , arg in enumerate ( sys . argv ) :
if arg == ' - ' :
if not sys . stdin . isatty ( ) :
2015-08-30 17:46:51 +02:00
sys . argv [ i ] = sys . stdin . read ( )
2015-08-17 09:46:50 +02:00
break
else :
2018-04-07 17:10:30 +02:00
raise Exception ( ' Cannot get argument from stdin ' )
2015-08-17 09:46:50 +02:00
elif arg == ' ? ' :
2017-01-22 21:25:24 +03:00
sys . argv [ i ] = input ( " Enter argument: " )
2015-10-28 09:33:35 +01:00
elif arg == ' : ' :
2023-09-08 15:46:06 +00:00
sys . argv [ i ] = prompt_password ( ' Enter argument (will not echo): ' , confirm = False )
2015-05-28 15:22:30 +02:00
# config is an object passed to the various constructors (wallet, interface, gui)
if is_android :
2022-07-08 13:32:41 +02:00
import importlib . util
2015-05-28 15:22:30 +02:00
config_options = {
2022-11-04 02:40:04 +00:00
' verbosity ' : ' * ' if util . is_android_debug_apk ( ) else ' ' ,
2015-09-07 16:44:17 +02:00
' cmd ' : ' gui ' ,
2023-08-30 13:11:33 +00:00
SimpleConfig . GUI_NAME . key ( ) : ' qml ' ,
2025-12-04 12:21:21 +01:00
SimpleConfig . WALLET_SHOULD_USE_SINGLE_PASSWORD . key ( ) : True ,
2015-05-28 15:22:30 +02:00
}
2025-07-29 12:00:40 +00:00
SimpleConfig . set_chain_config_opt_based_on_android_packagename ( config_options )
2015-05-28 15:22:30 +02:00
else :
2025-03-15 11:40:26 +01:00
# save sys args for next parser
saved_sys_argv = sys . argv [ : ]
# disable help, the next parser will display it
2025-03-16 15:00:24 +01:00
for x in sys . argv :
if x in [ ' -h ' , ' --help ' ] :
sys . argv . remove ( x )
2025-03-15 11:40:26 +01:00
# parse first without plugins
2025-03-16 15:00:24 +01:00
config_options = parse_command_line ( simple_parser = True )
2025-04-07 15:01:57 +02:00
tmp_config = read_config ( config_options )
2025-03-15 11:40:26 +01:00
# load (only) the commands modules of plugins so their commands are registered
2025-04-07 15:01:57 +02:00
_plugin_commands = Plugins ( tmp_config , cmd_only = True )
2025-03-15 11:40:26 +01:00
# re-parse command line
sys . argv = saved_sys_argv [ : ]
2025-03-15 11:44:24 +01:00
config_options = parse_command_line ( )
2015-05-30 06:56:45 +02:00
2025-04-07 15:01:57 +02:00
config = read_config ( config_options )
2023-05-05 17:00:18 +00:00
cmdname = config . get ( ' cmd ' )
2019-03-14 14:56:22 -07:00
2021-04-01 04:36:02 +02:00
# set language as early as possible
# Note: we are already too late for strings that are declared in the global scope
# of an already imported module. However, the GUI and the plugins at least have
2023-05-05 17:00:18 +00:00
# not been imported yet. (see #4621)
# Note: it is ok to call set_language() again later, but note that any call only applies
# to not-yet-evaluated strings.
2024-01-16 17:23:43 +00:00
# Note: the CLI is intentionally always non-localized.
# Note: Some unit tests might rely on the default non-localized strings.
2023-05-05 17:00:18 +00:00
if cmdname == ' gui ' :
2023-05-24 17:41:44 +00:00
gui_name = config . GUI_NAME
lang = config . LOCALIZATION_LANGUAGE
2023-05-05 17:00:18 +00:00
if not lang :
2023-08-09 14:43:49 +00:00
try :
from electrum . gui . default_lang import get_default_language
lang = get_default_language ( gui_name = gui_name )
_logger . info ( f " get_default_language: detected default as { lang =!r} " )
except ImportError as e :
_logger . info ( f " get_default_language: failed. got exc= { e !r} " )
2023-05-05 17:00:18 +00:00
set_language ( lang )
2021-04-01 04:36:02 +02:00
2025-05-29 18:14:40 +00:00
chain = config . get_selected_chain ( )
chain . set_as_network ( )
2019-09-06 15:08:15 +02:00
2023-07-07 20:48:29 +02:00
# check if we received a valid payment identifier
uri = config_options . get ( ' url ' )
if uri and not PaymentIdentifier ( None , uri ) . is_valid ( ) :
print_stderr ( ' unknown command: ' , uri )
sys . exit ( 1 )
2025-04-18 00:38:50 +00:00
if sys . platform == " linux " and not is_android :
import electrum . harden_memory_linux
electrum . harden_memory_linux . set_dumpable_safe ( False )
2019-09-02 19:04:08 +02:00
if cmdname == ' daemon ' and config . get ( " detach " ) :
2023-01-14 09:56:18 +01:00
# detect lockfile.
2023-01-25 15:35:42 +00:00
# This is not as good as get_file_descriptor, but that would require the asyncio loop
2023-01-14 09:56:18 +01:00
lockfile = daemon . get_lockfile ( config )
if os . path . exists ( lockfile ) :
print_stderr ( " Daemon already running (lockfile detected). " )
print_stderr ( " Run ' electrum stop ' to stop the daemon. " )
sys . exit ( 1 )
2023-08-03 22:42:08 +00:00
# Initialise rpc credentials to random if not set yet. This would normally be done
# later anyway, but we need to avoid the two sides of the fork setting conflicting random creds.
daemon . get_rpc_credentials ( config ) # inits creds as side-effect
2019-08-30 15:57:01 +02:00
# fork before creating the asyncio event loop
2023-05-16 12:28:59 +00:00
try :
pid = os . fork ( )
except AttributeError as e :
print_stderr ( f " Error: { e !r} " )
print_stderr ( " Running daemon in detached mode (-d) is not supported on this platform. " )
print_stderr ( " Try running the daemon in the foreground (without -d). " )
sys . exit ( 1 )
2019-08-15 13:17:16 +02:00
if pid :
2019-08-30 15:57:01 +02:00
print_stderr ( " starting daemon (PID %d ) " % pid )
2023-08-03 17:02:40 +00:00
loop , stop_loop , loop_thread = create_and_start_event_loop ( )
ready = daemon . wait_until_daemon_becomes_ready ( config = config , timeout = 5 )
if ready :
sys_exit ( 0 )
else :
print_stderr ( " timed out waiting for daemon to get ready " )
sys_exit ( 1 )
2019-08-15 13:17:16 +02:00
else :
# redirect standard file descriptors
sys . stdout . flush ( )
sys . stderr . flush ( )
si = open ( os . devnull , ' r ' )
so = open ( os . devnull , ' w ' )
se = open ( os . devnull , ' w ' )
os . dup2 ( si . fileno ( ) , sys . stdin . fileno ( ) )
os . dup2 ( so . fileno ( ) , sys . stdout . fileno ( ) )
os . dup2 ( se . fileno ( ) , sys . stderr . fileno ( ) )
2019-08-30 15:57:01 +02:00
loop , stop_loop , loop_thread = create_and_start_event_loop ( )
2020-10-05 17:07:33 +02:00
try :
handle_cmd (
cmdname = cmdname ,
config = config ,
config_options = config_options ,
)
except Exception :
_logger . exception ( " " )
sys_exit ( 1 )
def handle_cmd ( * , cmdname : str , config : ' SimpleConfig ' , config_options : dict ) :
2015-12-23 15:59:32 +01:00
if cmdname == ' gui ' :
2019-06-25 15:26:24 +02:00
configure_logging ( config )
2019-08-15 09:58:23 +02:00
fd = daemon . get_file_descriptor ( config )
2016-02-01 13:10:01 +01:00
if fd is not None :
2023-03-29 22:09:46 +00:00
d = daemon . Daemon ( config , fd , start_network = False )
2020-02-19 15:45:36 +01:00
try :
2023-08-24 16:56:23 +00:00
d . run_gui ( )
2020-02-19 15:45:36 +01:00
except BaseException as e :
_logger . exception ( ' daemon.run_gui errored ' )
sys_exit ( 1 )
else :
sys_exit ( 0 )
2016-02-01 09:25:57 +01:00
else :
2024-02-12 19:02:02 +00:00
try :
result = daemon . request ( config , ' gui ' , ( config_options , ) )
except JsonRPCError as e :
if e . code == JsonRPCError . Codes . USERFACING :
print_stderr ( e . message )
elif e . code == JsonRPCError . Codes . INTERNAL :
print_stderr ( " (inside daemon): " + e . data [ " traceback " ] )
print_stderr ( e . message )
else :
raise Exception ( f " unknown error code { e . code } " )
sys_exit ( 1 )
2016-02-01 09:00:10 +01:00
2015-12-23 15:59:32 +01:00
elif cmdname == ' daemon ' :
2019-09-02 19:04:08 +02:00
configure_logging ( config )
fd = daemon . get_file_descriptor ( config )
if fd is not None :
# run daemon
d = daemon . Daemon ( config , fd )
d . run_daemon ( )
sys_exit ( 0 )
2016-02-01 09:00:10 +01:00
else :
2020-12-08 12:21:56 +01:00
# FIXME this message is lost in detached mode (parent process already exited after forking)
2019-09-02 19:04:08 +02:00
print_msg ( " Daemon already running " )
sys_exit ( 1 )
2015-08-26 17:44:19 +02:00
else :
2015-12-23 15:59:32 +01:00
# command line
2022-06-12 00:54:35 +02:00
configure_logging ( config , log_to_file = False ) # don't spam logfiles for each client-side RPC, but support "-v"
2025-03-15 11:40:26 +01:00
cmd = known_commands [ cmdname ]
2025-06-03 10:24:40 +02:00
wallet_path = config . get_wallet_path ( )
2025-05-31 11:45:57 +02:00
if cmd . requires_wallet and not wallet_path :
print_stderr ( ' wallet path not provided ' )
sys_exit ( 1 )
2023-05-24 17:41:44 +00:00
if not config . NETWORK_OFFLINE :
2023-09-08 15:46:06 +00:00
init_cmdline ( config_options , wallet_path , rpcserver = True , config = config )
2023-05-24 17:41:44 +00:00
timeout = config . CLI_TIMEOUT
2019-08-31 09:19:46 +02:00
try :
result = daemon . request ( config , ' run_cmdline ' , ( config_options , ) , timeout )
except daemon . DaemonNotRunning :
2019-09-02 19:04:08 +02:00
print_msg ( " Daemon not running; try ' electrum daemon -d ' " )
2019-08-31 09:19:46 +02:00
if not cmd . requires_network :
print_msg ( " To run this command without a daemon, use --offline " )
2023-01-25 15:35:42 +00:00
if cmd . name == " stop " : # remove lockfile if it exists, as daemon looks dead
lockfile = daemon . get_lockfile ( config )
if os . path . exists ( lockfile ) :
print_msg ( " Found lingering lockfile for daemon. Removing. " )
daemon . remove_lockfile ( lockfile )
2019-08-15 13:17:16 +02:00
sys_exit ( 1 )
2024-02-12 19:02:02 +00:00
except JsonRPCError as e :
if e . code == JsonRPCError . Codes . USERFACING :
print_stderr ( e . message )
elif e . code == JsonRPCError . Codes . INTERNAL :
print_stderr ( " (inside daemon): " + e . data [ " traceback " ] )
print_stderr ( e . message )
else :
raise Exception ( f " unknown error code { e . code } " )
sys_exit ( 1 )
2019-08-31 09:19:46 +02:00
except Exception as e :
2024-02-12 19:02:02 +00:00
_logger . exception ( " error running command (with daemon) " )
2019-08-31 09:19:46 +02:00
sys_exit ( 1 )
else :
if cmd . requires_network :
print_msg ( " This command cannot be run offline " )
sys_exit ( 1 )
2023-08-17 07:52:40 +02:00
lockfile = daemon . get_lockfile ( config )
if os . path . exists ( lockfile ) :
print_stderr ( " Daemon already running (lockfile detected) " )
print_stderr ( " Run ' electrum stop ' to stop the daemon. " )
print_stderr ( " Run this command without --offline to interact with the daemon " )
sys_exit ( 1 )
2023-09-08 15:46:06 +00:00
init_cmdline ( config_options , wallet_path , rpcserver = False , config = config )
2025-03-06 11:43:50 +01:00
plugins = Plugins ( config , ' cmdline ' )
2025-05-31 11:45:57 +02:00
coro = run_offline_command ( config , config_options , wallet_path , plugins )
2019-08-31 09:19:46 +02:00
fut = asyncio . run_coroutine_threadsafe ( coro , loop )
try :
2023-08-24 16:56:23 +00:00
try :
result = fut . result ( )
finally :
plugins . stop ( )
plugins . stopped_event . wait ( 1 )
2024-02-12 19:02:02 +00:00
except UserFacingException as e :
print_stderr ( str ( e ) )
sys_exit ( 1 )
2025-08-26 12:02:33 +02:00
except InvalidPassword :
print_stderr ( " Invalid password " )
sys_exit ( 1 )
except UserCancelled :
print_stderr ( " Aborted by user " )
sys_exit ( 1 )
2019-08-31 09:19:46 +02:00
except Exception as e :
2024-02-12 19:02:02 +00:00
_logger . exception ( " error running command (without daemon) " )
2019-08-31 09:19:46 +02:00
sys_exit ( 1 )
2024-02-12 19:02:02 +00:00
# print result
2017-10-24 14:04:16 +02:00
if isinstance ( result , str ) :
2015-12-23 15:59:32 +01:00
print_msg ( result )
elif result is not None :
print_msg ( json_encode ( result ) )
2019-08-15 13:17:16 +02:00
sys_exit ( 0 )
2020-10-05 17:07:33 +02:00
if __name__ == ' __main__ ' :
main ( )