trun_electrum - electrum - Electrum Bitcoin wallet
HTML 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()