|
| 1 | +import os |
| 2 | +import sys |
| 3 | +import warnings |
| 4 | + |
| 5 | + |
| 6 | +MIN_PYTHON_VERSION = "3.6.1" # FIXME duplicated from setup.py |
| 7 | +_min_python_version_tuple = tuple(map(int, (MIN_PYTHON_VERSION.split(".")))) |
| 8 | + |
| 9 | + |
| 10 | +if sys.version_info[:3] < _min_python_version_tuple: |
| 11 | + sys.exit("Error: Electrum requires Python version >= %s..." % MIN_PYTHON_VERSION) |
| 12 | + |
| 13 | + |
| 14 | +def check_imports(): |
| 15 | + # pure-python dependencies need to be imported here for pyinstaller |
| 16 | + try: |
| 17 | + import dns |
| 18 | + import pyaes |
| 19 | + import ecdsa |
| 20 | + import certifi |
| 21 | + import qrcode |
| 22 | + import google.protobuf |
| 23 | + import jsonrpclib |
| 24 | + import aiorpcx |
| 25 | + except ImportError as e: |
| 26 | + sys.exit(f"Error: {str(e)}. Try 'sudo python3 -m pip install <module-name>'") |
| 27 | + # the following imports are for pyinstaller |
| 28 | + from google.protobuf import descriptor |
| 29 | + from google.protobuf import message |
| 30 | + from google.protobuf import reflection |
| 31 | + from google.protobuf import descriptor_pb2 |
| 32 | + from jsonrpclib import SimpleJSONRPCServer |
| 33 | + # make sure that certificates are here |
| 34 | + assert os.path.exists(certifi.where()) |
| 35 | + |
| 36 | +check_imports() |
| 37 | + |
| 38 | +from electrum.logging import get_logger, configure_logging |
| 39 | +from electrum import util |
| 40 | +from electrum import constants |
| 41 | +from electrum import SimpleConfig |
| 42 | +from electrum.wallet import Wallet |
| 43 | +from electrum.storage import WalletStorage, get_derivation_used_for_hw_device_encryption |
| 44 | +from electrum.util import print_msg, print_stderr, json_encode, json_decode, UserCancelled |
| 45 | +from electrum.util import InvalidPassword |
| 46 | +from electrum.commands import get_parser, known_commands, Commands, config_variables |
| 47 | +from electrum import daemon |
| 48 | +from electrum import keystore |
| 49 | + |
| 50 | +_logger = get_logger(__name__) |
| 51 | + |
| 52 | + |
| 53 | +# get password routine |
| 54 | +def prompt_password(prompt, confirm=True): |
| 55 | + import getpass |
| 56 | + password = getpass.getpass(prompt, stream=None) |
| 57 | + if password and confirm: |
| 58 | + password2 = getpass.getpass("Confirm: ") |
| 59 | + if password != password2: |
| 60 | + sys.exit("Error: Passwords do not match.") |
| 61 | + if not password: |
| 62 | + password = None |
| 63 | + return password |
| 64 | + |
| 65 | + |
| 66 | +def init_daemon(config_options): |
| 67 | + config = SimpleConfig(config_options) |
| 68 | + storage = WalletStorage(config.get_wallet_path()) |
| 69 | + if not storage.file_exists(): |
| 70 | + print_msg("Error: Wallet file not found.") |
| 71 | + print_msg("Type 'electrum create' to create a new wallet, or provide a path to a wallet with the -w option") |
| 72 | + sys.exit(0) |
| 73 | + if storage.is_encrypted(): |
| 74 | + if storage.is_encrypted_with_hw_device(): |
| 75 | + plugins = init_plugins(config, 'cmdline') |
| 76 | + password = get_password_for_hw_device_encrypted_storage(plugins) |
| 77 | + elif config.get('password'): |
| 78 | + password = config.get('password') |
| 79 | + else: |
| 80 | + password = prompt_password('Password:', False) |
| 81 | + if not password: |
| 82 | + print_msg("Error: Password required") |
| 83 | + sys.exit(1) |
| 84 | + else: |
| 85 | + password = None |
| 86 | + config_options['password'] = password |
| 87 | + |
| 88 | + |
| 89 | +def init_cmdline(config_options, server): |
| 90 | + config = SimpleConfig(config_options) |
| 91 | + cmdname = config.get('cmd') |
| 92 | + cmd = known_commands[cmdname] |
| 93 | + |
| 94 | + if cmdname == 'signtransaction' and config.get('privkey'): |
| 95 | + cmd.requires_wallet = False |
| 96 | + cmd.requires_password = False |
| 97 | + |
| 98 | + if cmdname in ['payto', 'paytomany'] and config.get('unsigned'): |
| 99 | + cmd.requires_password = False |
| 100 | + |
| 101 | + if cmdname in ['payto', 'paytomany'] and config.get('broadcast'): |
| 102 | + cmd.requires_network = True |
| 103 | + |
| 104 | + # instantiate wallet for command-line |
| 105 | + storage = WalletStorage(config.get_wallet_path()) |
| 106 | + |
| 107 | + if cmd.requires_wallet and not storage.file_exists(): |
| 108 | + print_msg("Error: Wallet file not found.") |
| 109 | + print_msg("Type 'electrum create' to create a new wallet, or provide a path to a wallet with the -w option") |
| 110 | + sys.exit(0) |
| 111 | + |
| 112 | + # important warning |
| 113 | + if cmd.name in ['getprivatekeys']: |
| 114 | + print_stderr("WARNING: ALL your private keys are secret.") |
| 115 | + print_stderr("Exposing a single private key can compromise your entire wallet!") |
| 116 | + print_stderr("In particular, DO NOT use 'redeem private key' services proposed by third parties.") |
| 117 | + |
| 118 | + # commands needing password |
| 119 | + if (cmd.requires_wallet and storage.is_encrypted() and server is None)\ |
| 120 | + or (cmd.requires_password and (storage.is_encrypted() or storage.get('use_encryption'))): |
| 121 | + if storage.is_encrypted_with_hw_device(): |
| 122 | + # this case is handled later in the control flow |
| 123 | + password = None |
| 124 | + elif config.get('password'): |
| 125 | + password = config.get('password') |
| 126 | + else: |
| 127 | + password = prompt_password('Password:', False) |
| 128 | + if not password: |
| 129 | + print_msg("Error: Password required") |
| 130 | + sys.exit(1) |
| 131 | + else: |
| 132 | + password = None |
| 133 | + |
| 134 | + config_options['password'] = config_options.get('password') or password |
| 135 | + |
| 136 | + if cmd.name == 'password': |
| 137 | + new_password = prompt_password('New password:') |
| 138 | + config_options['new_password'] = new_password |
| 139 | + |
| 140 | + |
| 141 | +def get_connected_hw_devices(plugins): |
| 142 | + supported_plugins = plugins.get_hardware_support() |
| 143 | + # scan devices |
| 144 | + devices = [] |
| 145 | + devmgr = plugins.device_manager |
| 146 | + for splugin in supported_plugins: |
| 147 | + name, plugin = splugin.name, splugin.plugin |
| 148 | + if not plugin: |
| 149 | + e = splugin.exception |
| 150 | + _logger.error(f"{name}: error during plugin init: {repr(e)}") |
| 151 | + continue |
| 152 | + try: |
| 153 | + u = devmgr.unpaired_device_infos(None, plugin) |
| 154 | + except Exception as e: |
| 155 | + _logger.error(f'error getting device infos for {name}: {repr(e)}') |
| 156 | + continue |
| 157 | + devices += list(map(lambda x: (name, x), u)) |
| 158 | + return devices |
| 159 | + |
| 160 | + |
| 161 | +def get_password_for_hw_device_encrypted_storage(plugins): |
| 162 | + devices = get_connected_hw_devices(plugins) |
| 163 | + if len(devices) == 0: |
| 164 | + print_msg("Error: No connected hw device found. Cannot decrypt this wallet.") |
| 165 | + sys.exit(1) |
| 166 | + elif len(devices) > 1: |
| 167 | + print_msg("Warning: multiple hardware devices detected. " |
| 168 | + "The first one will be used to decrypt the wallet.") |
| 169 | + # FIXME we use the "first" device, in case of multiple ones |
| 170 | + name, device_info = devices[0] |
| 171 | + plugin = plugins.get_plugin(name) |
| 172 | + derivation = get_derivation_used_for_hw_device_encryption() |
| 173 | + try: |
| 174 | + xpub = plugin.get_xpub(device_info.device.id_, derivation, 'standard', plugin.handler) |
| 175 | + except UserCancelled: |
| 176 | + sys.exit(0) |
| 177 | + password = keystore.Xpub.get_pubkey_from_xpub(xpub, ()) |
| 178 | + return password |
| 179 | + |
| 180 | + |
| 181 | +def run_offline_command(config, config_options, plugins): |
| 182 | + print(config) |
| 183 | + print(config_options) |
| 184 | + print(plugins) |
| 185 | + cmdname = config.get('cmd') |
| 186 | + cmd = known_commands[cmdname] |
| 187 | + print(cmd.__dict__) |
| 188 | + password = config_options.get('password') |
| 189 | + if cmd.requires_wallet: |
| 190 | + storage = WalletStorage(config.get_wallet_path()) |
| 191 | + if storage.is_encrypted(): |
| 192 | + if storage.is_encrypted_with_hw_device(): |
| 193 | + password = get_password_for_hw_device_encrypted_storage(plugins) |
| 194 | + config_options['password'] = password |
| 195 | + storage.decrypt(password) |
| 196 | + wallet = Wallet(storage) |
| 197 | + else: |
| 198 | + wallet = None |
| 199 | + # check password |
| 200 | + if cmd.requires_password and wallet.has_password(): |
| 201 | + try: |
| 202 | + wallet.check_password(password) |
| 203 | + except InvalidPassword: |
| 204 | + print_msg("Error: This password does not decode this wallet.") |
| 205 | + sys.exit(1) |
| 206 | + if cmd.requires_network: |
| 207 | + print_msg("Warning: running command offline") |
| 208 | + # arguments passed to function |
| 209 | + args = [config.get(x) for x in cmd.params] |
| 210 | + # decode json arguments |
| 211 | + if cmdname not in ('setconfig',): |
| 212 | + args = list(map(json_decode, args)) |
| 213 | + # options |
| 214 | + kwargs = {} |
| 215 | + for x in cmd.options: |
| 216 | + kwargs[x] = (config_options.get(x) if x in ['password', 'new_password'] else config.get(x)) |
| 217 | + cmd_runner = Commands(config, wallet, None) |
| 218 | + func = getattr(cmd_runner, cmd.name) |
| 219 | + result = func(*args, **kwargs) |
| 220 | + # save wallet |
| 221 | + if wallet: |
| 222 | + wallet.storage.write() |
| 223 | + return result |
| 224 | + |
| 225 | + |
| 226 | +def init_plugins(config, gui_name): |
| 227 | + from electrum.plugin import Plugins |
| 228 | + return Plugins(config, gui_name) |
| 229 | + |
| 230 | + |
| 231 | +class WalletScripting(object): |
| 232 | + @classmethod |
| 233 | + def setup(cls): |
| 234 | + pass |
| 235 | + |
| 236 | + @classmethod |
| 237 | + def call(cls, command, *args, **kwargs): |
| 238 | + # command line |
| 239 | + config_options = { |
| 240 | + 'verbosity': '', |
| 241 | + 'verbosity_shortcuts': '', |
| 242 | + 'portable': False, |
| 243 | + 'testnet': False, |
| 244 | + 'regtest': False, |
| 245 | + 'simnet': False, |
| 246 | + 'cmd': command |
| 247 | + } |
| 248 | + config_options['cwd'] = os.getcwd() |
| 249 | + config = SimpleConfig(config_options) |
| 250 | + cmdname = config.get('cmd') |
| 251 | + |
| 252 | + server = daemon.get_server(config) |
| 253 | + init_cmdline(config_options, server) |
| 254 | + if server is not None: |
| 255 | + print("goes online") |
| 256 | + result = server.run_cmdline(config_options) |
| 257 | + print(result) |
| 258 | + else: |
| 259 | + print("goes offline") |
| 260 | + cmd = known_commands[cmdname] |
| 261 | + if cmd.requires_network: |
| 262 | + print_msg("Daemon not running; try 'electrum daemon start'") |
| 263 | + sys.exit(1) |
| 264 | + else: |
| 265 | + plugins = init_plugins(config, 'cmdline') |
| 266 | + result = run_offline_command(config, config_options, plugins) |
| 267 | + print(result) |
| 268 | + |
| 269 | + |
0 commit comments