2015-11-30 10:09:54 +01:00
|
|
|
#!/usr/bin/env python
|
|
|
|
|
#
|
|
|
|
|
# Electrum - lightweight Bitcoin client
|
|
|
|
|
# Copyright (C) 2015 Thomas Voegtlin
|
|
|
|
|
#
|
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:
|
2015-11-30 10:09:54 +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.
|
2015-11-30 10:09:54 +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.
|
2018-09-06 16:18:45 +02:00
|
|
|
import asyncio
|
2016-01-31 11:43:11 +09:00
|
|
|
import ast
|
|
|
|
|
import os
|
2016-01-31 14:40:22 +09:00
|
|
|
import time
|
2018-02-15 17:30:40 +01:00
|
|
|
import traceback
|
|
|
|
|
import sys
|
2018-09-25 16:38:26 +02:00
|
|
|
import threading
|
2021-03-09 17:52:36 +01:00
|
|
|
from typing import Dict, Optional, Tuple, Iterable, Callable, Union, Sequence, Mapping, TYPE_CHECKING
|
2020-02-04 18:17:12 +01:00
|
|
|
from base64 import b64decode, b64encode
|
2019-09-03 14:44:33 +02:00
|
|
|
from collections import defaultdict
|
2020-06-09 17:45:04 +02:00
|
|
|
import json
|
2021-11-05 23:55:55 +01:00
|
|
|
import socket
|
2019-07-30 11:47:17 +02:00
|
|
|
|
2019-12-10 22:55:11 +01:00
|
|
|
import aiohttp
|
|
|
|
|
from aiohttp import web, client_exceptions
|
2022-02-08 12:34:49 +01:00
|
|
|
from aiorpcx import timeout_after, TaskTimeout, ignore_after
|
2015-11-30 10:09:54 +01:00
|
|
|
|
2020-04-14 16:12:47 +02:00
|
|
|
from . import util
|
2017-01-22 21:25:24 +03:00
|
|
|
from .network import Network
|
2019-08-15 13:17:16 +02:00
|
|
|
from .util import (json_decode, to_bytes, to_string, profiler, standardize_path, constant_time_compare)
|
2020-05-31 12:49:49 +02:00
|
|
|
from .invoices import PR_PAID, PR_EXPIRED
|
2022-02-08 12:34:49 +01:00
|
|
|
from .util import log_exceptions, ignore_exceptions, randrange, OldTaskGroup
|
2022-06-22 01:24:19 +02:00
|
|
|
from .util import EventListener, event_listener
|
2018-09-28 17:58:46 +02:00
|
|
|
from .wallet import Wallet, Abstract_Wallet
|
2017-01-22 21:25:24 +03:00
|
|
|
from .storage import WalletStorage
|
2020-02-05 15:13:37 +01:00
|
|
|
from .wallet_db import WalletDB
|
2017-01-22 21:25:24 +03:00
|
|
|
from .commands import known_commands, Commands
|
|
|
|
|
from .simple_config import SimpleConfig
|
|
|
|
|
from .exchange_rate import FxThread
|
2019-07-30 11:47:17 +02:00
|
|
|
from .logging import get_logger, Logger
|
2022-04-11 16:53:25 +02:00
|
|
|
from . import GuiImportError
|
2022-10-28 11:55:27 +02:00
|
|
|
from .plugin import run_hook
|
2019-04-26 18:52:26 +02:00
|
|
|
|
2021-03-09 17:52:36 +01:00
|
|
|
if TYPE_CHECKING:
|
|
|
|
|
from electrum import gui
|
|
|
|
|
|
2019-04-26 18:52:26 +02:00
|
|
|
|
|
|
|
|
_logger = get_logger(__name__)
|
2015-11-30 10:09:54 +01:00
|
|
|
|
2019-12-10 22:55:11 +01:00
|
|
|
|
2019-08-15 12:25:59 +02:00
|
|
|
class DaemonNotRunning(Exception):
|
|
|
|
|
pass
|
2017-01-30 12:36:56 +03:00
|
|
|
|
2021-10-25 12:00:00 +00:00
|
|
|
def get_rpcsock_defaultpath(config: SimpleConfig):
|
|
|
|
|
return os.path.join(config.path, 'daemon_rpc_socket')
|
|
|
|
|
|
2022-03-02 12:42:49 +01:00
|
|
|
def get_rpcsock_default_type(config: SimpleConfig):
|
2022-05-13 16:19:28 +02:00
|
|
|
if config.get('rpcport'):
|
2022-03-02 12:42:49 +01:00
|
|
|
return 'tcp'
|
2021-11-05 23:55:55 +01:00
|
|
|
# Use unix domain sockets when available,
|
|
|
|
|
# with the extra paranoia that in case windows "implements" them,
|
|
|
|
|
# we want to test it before making it the default there.
|
|
|
|
|
if hasattr(socket, 'AF_UNIX') and sys.platform != 'win32':
|
|
|
|
|
return 'unix'
|
2021-10-25 12:00:00 +00:00
|
|
|
return 'tcp'
|
|
|
|
|
|
2018-10-22 16:41:25 +02:00
|
|
|
def get_lockfile(config: SimpleConfig):
|
2016-02-01 10:20:22 +01:00
|
|
|
return os.path.join(config.path, 'daemon')
|
|
|
|
|
|
|
|
|
|
def remove_lockfile(lockfile):
|
|
|
|
|
os.unlink(lockfile)
|
|
|
|
|
|
2017-01-30 12:36:56 +03:00
|
|
|
|
2019-08-15 09:58:23 +02:00
|
|
|
def get_file_descriptor(config: SimpleConfig):
|
2016-02-01 10:20:22 +01:00
|
|
|
'''Tries to create the lockfile, using O_EXCL to
|
2022-02-16 17:40:30 +01:00
|
|
|
prevent races. If it succeeds, it returns the FD.
|
|
|
|
|
Otherwise, try and connect to the server specified in the lockfile.
|
|
|
|
|
If this succeeds, the server is returned. Otherwise, remove the
|
2016-02-01 10:20:22 +01:00
|
|
|
lockfile and try again.'''
|
|
|
|
|
lockfile = get_lockfile(config)
|
|
|
|
|
while True:
|
|
|
|
|
try:
|
2019-08-15 09:58:23 +02:00
|
|
|
return os.open(lockfile, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o644)
|
2016-02-01 10:20:22 +01:00
|
|
|
except OSError:
|
|
|
|
|
pass
|
2019-08-15 12:25:59 +02:00
|
|
|
try:
|
|
|
|
|
request(config, 'ping')
|
|
|
|
|
return None
|
|
|
|
|
except DaemonNotRunning:
|
|
|
|
|
# Couldn't connect; remove lockfile and try again.
|
|
|
|
|
remove_lockfile(lockfile)
|
2016-02-01 10:20:22 +01:00
|
|
|
|
2017-01-30 12:36:56 +03:00
|
|
|
|
2019-08-15 13:17:16 +02:00
|
|
|
|
|
|
|
|
def request(config: SimpleConfig, endpoint, args=(), timeout=60):
|
2016-02-01 10:20:22 +01:00
|
|
|
lockfile = get_lockfile(config)
|
|
|
|
|
while True:
|
|
|
|
|
create_time = None
|
|
|
|
|
try:
|
|
|
|
|
with open(lockfile) as f:
|
2021-10-25 12:00:00 +00:00
|
|
|
socktype, address, create_time = ast.literal_eval(f.read())
|
|
|
|
|
if socktype == 'unix':
|
|
|
|
|
path = address
|
|
|
|
|
(host, port) = "127.0.0.1", 0
|
|
|
|
|
# We still need a host and port for e.g. HTTP Host header
|
|
|
|
|
elif socktype == 'tcp':
|
|
|
|
|
(host, port) = address
|
|
|
|
|
else:
|
|
|
|
|
raise Exception(f"corrupt lockfile; socktype={socktype!r}")
|
2019-08-15 12:25:59 +02:00
|
|
|
except Exception:
|
|
|
|
|
raise DaemonNotRunning()
|
2019-08-15 13:17:16 +02:00
|
|
|
rpc_user, rpc_password = get_rpc_credentials(config)
|
|
|
|
|
server_url = 'http://%s:%d' % (host, port)
|
|
|
|
|
auth = aiohttp.BasicAuth(login=rpc_user, password=rpc_password)
|
asyncio: stop using get_event_loop(). introduce ~singleton loop.
asyncio.get_event_loop() became deprecated in python3.10. (see https://github.com/python/cpython/issues/83710)
```
.../electrum/electrum/daemon.py:470: DeprecationWarning: There is no current event loop
self.asyncio_loop = asyncio.get_event_loop()
.../electrum/electrum/network.py:276: DeprecationWarning: There is no current event loop
self.asyncio_loop = asyncio.get_event_loop()
```
Also, according to that thread, "set_event_loop() [... is] not deprecated by oversight".
So, we stop using get_event_loop() and set_event_loop() in our own code.
Note that libraries we use (such as the stdlib for python <3.10), might call get_event_loop,
which then relies on us having called set_event_loop e.g. for the GUI thread. To work around
this, a custom event loop policy providing a get_event_loop implementation is used.
Previously, we have been using a single asyncio event loop, created with
util.create_and_start_event_loop, and code in many places got a reference to this loop
using asyncio.get_event_loop().
Now, we still use a single asyncio event loop, but it is now stored as a global in
util._asyncio_event_loop (access with util.get_asyncio_loop()).
I believe these changes also fix https://github.com/spesmilo/electrum/issues/5376
2022-04-29 18:24:49 +02:00
|
|
|
loop = util.get_asyncio_loop()
|
2019-08-15 13:17:16 +02:00
|
|
|
async def request_coroutine():
|
2021-10-25 12:00:00 +00:00
|
|
|
if socktype == 'unix':
|
|
|
|
|
connector = aiohttp.UnixConnector(path=path)
|
|
|
|
|
elif socktype == 'tcp':
|
|
|
|
|
connector = None # This will transform into TCP.
|
|
|
|
|
else:
|
|
|
|
|
raise Exception(f"impossible socktype ({socktype!r})")
|
|
|
|
|
async with aiohttp.ClientSession(auth=auth, connector=connector) as session:
|
2020-06-09 17:50:06 +02:00
|
|
|
c = util.JsonRPCClient(session, server_url)
|
2020-06-09 10:05:23 +02:00
|
|
|
return await c.request(endpoint, *args)
|
2019-08-15 12:25:59 +02:00
|
|
|
try:
|
2019-08-15 13:17:16 +02:00
|
|
|
fut = asyncio.run_coroutine_threadsafe(request_coroutine(), loop)
|
|
|
|
|
return fut.result(timeout=timeout)
|
|
|
|
|
except aiohttp.client_exceptions.ClientConnectorError as e:
|
|
|
|
|
_logger.info(f"failed to connect to JSON-RPC server {e}")
|
2019-08-15 12:25:59 +02:00
|
|
|
if not create_time or create_time < time.time() - 1.0:
|
|
|
|
|
raise DaemonNotRunning()
|
2016-02-01 10:20:22 +01:00
|
|
|
# Sleep a bit and try again; it might have just been started
|
|
|
|
|
time.sleep(1.0)
|
|
|
|
|
|
|
|
|
|
|
2018-10-22 16:41:25 +02:00
|
|
|
def get_rpc_credentials(config: SimpleConfig) -> Tuple[str, str]:
|
2018-01-07 19:26:59 +01:00
|
|
|
rpc_user = config.get('rpcuser', None)
|
|
|
|
|
rpc_password = config.get('rpcpassword', None)
|
2020-11-25 11:47:25 +01:00
|
|
|
if rpc_user == '':
|
|
|
|
|
rpc_user = None
|
|
|
|
|
if rpc_password == '':
|
|
|
|
|
rpc_password = None
|
2018-01-07 19:26:59 +01:00
|
|
|
if rpc_user is None or rpc_password is None:
|
|
|
|
|
rpc_user = 'user'
|
|
|
|
|
bits = 128
|
|
|
|
|
nbytes = bits // 8 + (bits % 8 > 0)
|
2020-02-04 18:17:12 +01:00
|
|
|
pw_int = randrange(pow(2, bits))
|
|
|
|
|
pw_b64 = b64encode(
|
2018-01-07 19:26:59 +01:00
|
|
|
pw_int.to_bytes(nbytes, 'big'), b'-_')
|
|
|
|
|
rpc_password = to_string(pw_b64, 'ascii')
|
|
|
|
|
config.set_key('rpcuser', rpc_user)
|
|
|
|
|
config.set_key('rpcpassword', rpc_password, save=True)
|
|
|
|
|
return rpc_user, rpc_password
|
|
|
|
|
|
|
|
|
|
|
2020-05-12 10:02:22 +02:00
|
|
|
class AuthenticationError(Exception):
|
|
|
|
|
pass
|
2018-10-13 12:11:10 +02:00
|
|
|
|
2020-05-12 10:02:22 +02:00
|
|
|
class AuthenticationInvalidOrMissing(AuthenticationError):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
class AuthenticationCredentialsInvalid(AuthenticationError):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
class AuthenticatedServer(Logger):
|
|
|
|
|
|
|
|
|
|
def __init__(self, rpc_user, rpc_password):
|
2019-07-30 11:47:17 +02:00
|
|
|
Logger.__init__(self)
|
2020-05-12 10:02:22 +02:00
|
|
|
self.rpc_user = rpc_user
|
|
|
|
|
self.rpc_password = rpc_password
|
|
|
|
|
self.auth_lock = asyncio.Lock()
|
2020-06-09 17:45:04 +02:00
|
|
|
self._methods = {} # type: Dict[str, Callable]
|
|
|
|
|
|
|
|
|
|
def register_method(self, f):
|
|
|
|
|
assert f.__name__ not in self._methods, f"name collision for {f.__name__}"
|
|
|
|
|
self._methods[f.__name__] = f
|
2020-05-12 10:02:22 +02:00
|
|
|
|
|
|
|
|
async def authenticate(self, headers):
|
|
|
|
|
if self.rpc_password == '':
|
|
|
|
|
# RPC authentication is disabled
|
|
|
|
|
return
|
|
|
|
|
auth_string = headers.get('Authorization', None)
|
|
|
|
|
if auth_string is None:
|
|
|
|
|
raise AuthenticationInvalidOrMissing('CredentialsMissing')
|
|
|
|
|
basic, _, encoded = auth_string.partition(' ')
|
|
|
|
|
if basic != 'Basic':
|
|
|
|
|
raise AuthenticationInvalidOrMissing('UnsupportedType')
|
|
|
|
|
encoded = to_bytes(encoded, 'utf8')
|
|
|
|
|
credentials = to_string(b64decode(encoded), 'utf8')
|
|
|
|
|
username, _, password = credentials.partition(':')
|
|
|
|
|
if not (constant_time_compare(username, self.rpc_user)
|
|
|
|
|
and constant_time_compare(password, self.rpc_password)):
|
|
|
|
|
await asyncio.sleep(0.050)
|
|
|
|
|
raise AuthenticationCredentialsInvalid('Invalid Credentials')
|
|
|
|
|
|
|
|
|
|
async def handle(self, request):
|
|
|
|
|
async with self.auth_lock:
|
|
|
|
|
try:
|
|
|
|
|
await self.authenticate(request.headers)
|
|
|
|
|
except AuthenticationInvalidOrMissing:
|
|
|
|
|
return web.Response(headers={"WWW-Authenticate": "Basic realm=Electrum"},
|
|
|
|
|
text='Unauthorized', status=401)
|
|
|
|
|
except AuthenticationCredentialsInvalid:
|
|
|
|
|
return web.Response(text='Forbidden', status=403)
|
2020-06-09 10:05:23 +02:00
|
|
|
try:
|
|
|
|
|
request = await request.text()
|
|
|
|
|
request = json.loads(request)
|
|
|
|
|
method = request['method']
|
|
|
|
|
_id = request['id']
|
2020-06-09 17:45:04 +02:00
|
|
|
params = request.get('params', []) # type: Union[Sequence, Mapping]
|
|
|
|
|
if method not in self._methods:
|
|
|
|
|
raise Exception(f"attempting to use unregistered method: {method}")
|
|
|
|
|
f = self._methods[method]
|
|
|
|
|
except Exception as e:
|
|
|
|
|
self.logger.exception("invalid request")
|
2020-06-09 10:05:23 +02:00
|
|
|
return web.Response(text='Invalid Request', status=500)
|
2020-09-23 21:39:31 +02:00
|
|
|
response = {
|
|
|
|
|
'id': _id,
|
|
|
|
|
'jsonrpc': '2.0',
|
|
|
|
|
}
|
2020-06-09 10:05:23 +02:00
|
|
|
try:
|
2020-06-09 17:45:04 +02:00
|
|
|
if isinstance(params, dict):
|
|
|
|
|
response['result'] = await f(**params)
|
|
|
|
|
else:
|
|
|
|
|
response['result'] = await f(*params)
|
2020-06-09 10:05:23 +02:00
|
|
|
except BaseException as e:
|
2020-06-09 17:45:04 +02:00
|
|
|
self.logger.exception("internal error while executing RPC")
|
2020-11-14 19:59:59 +01:00
|
|
|
response['error'] = {
|
|
|
|
|
'code': 1,
|
|
|
|
|
'message': str(e),
|
|
|
|
|
}
|
2020-06-09 10:05:23 +02:00
|
|
|
return web.json_response(response)
|
2020-05-12 10:02:22 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class CommandsServer(AuthenticatedServer):
|
|
|
|
|
|
|
|
|
|
def __init__(self, daemon, fd):
|
|
|
|
|
rpc_user, rpc_password = get_rpc_credentials(daemon.config)
|
|
|
|
|
AuthenticatedServer.__init__(self, rpc_user, rpc_password)
|
|
|
|
|
self.daemon = daemon
|
|
|
|
|
self.fd = fd
|
|
|
|
|
self.config = daemon.config
|
2021-10-25 12:00:00 +00:00
|
|
|
sockettype = self.config.get('rpcsock', 'auto')
|
2022-03-02 12:42:49 +01:00
|
|
|
self.socktype = sockettype if sockettype != 'auto' else get_rpcsock_default_type(self.config)
|
2021-10-25 12:00:00 +00:00
|
|
|
self.sockpath = self.config.get('rpcsockpath', get_rpcsock_defaultpath(self.config))
|
2020-05-12 10:02:22 +02:00
|
|
|
self.host = self.config.get('rpchost', '127.0.0.1')
|
|
|
|
|
self.port = self.config.get('rpcport', 0)
|
|
|
|
|
self.app = web.Application()
|
|
|
|
|
self.app.router.add_post("/", self.handle)
|
2020-06-09 17:45:04 +02:00
|
|
|
self.register_method(self.ping)
|
|
|
|
|
self.register_method(self.gui)
|
2020-05-12 10:02:22 +02:00
|
|
|
self.cmd_runner = Commands(config=self.config, network=self.daemon.network, daemon=self.daemon)
|
|
|
|
|
for cmdname in known_commands:
|
2020-06-09 17:45:04 +02:00
|
|
|
self.register_method(getattr(self.cmd_runner, cmdname))
|
|
|
|
|
self.register_method(self.run_cmdline)
|
2020-05-12 10:02:22 +02:00
|
|
|
|
daemon: better error msg if rpchost/rpcport is badly configured
old traceback:
```
$ ./run_electrum --testnet -o setconfig rpchost qweasdfcsdf
$ ./run_electrum --testnet -o setconfig rpcport 7777
$ ./run_electrum --testnet daemon
E | daemon.Daemon | taskgroup died.
Traceback (most recent call last):
File "/home/user/wspace/electrum/electrum/daemon.py", line 419, in _run
async with self.taskgroup as group:
File "/home/user/wspace/electrum/packages/aiorpcx/curio.py", line 297, in __aexit__
await self.join()
File "/home/user/wspace/electrum/electrum/util.py", line 1335, in join
task.result()
File "/home/user/wspace/electrum/electrum/daemon.py", line 281, in run
await site.start() #
File "/home/user/wspace/electrum/packages/aiohttp/web_runner.py", line 121, in start
self._server = await loop.create_server(
File "/usr/lib/python3.10/asyncio/base_events.py", line 1471, in create_server
infos = await tasks.gather(*fs)
File "/usr/lib/python3.10/asyncio/base_events.py", line 1408, in _create_server_getaddrinfo
infos = await self._ensure_resolved((host, port), family=family,
File "/usr/lib/python3.10/asyncio/base_events.py", line 1404, in _ensure_resolved
return await loop.getaddrinfo(host, port, family=family, type=type,
File "/usr/lib/python3.10/asyncio/base_events.py", line 860, in getaddrinfo
return await self.run_in_executor(
File "/usr/lib/python3.10/concurrent/futures/thread.py", line 58, in run
result = self.fn(*self.args, **self.kwargs)
File "/usr/lib/python3.10/socket.py", line 955, in getaddrinfo
for res in _socket.getaddrinfo(host, port, family, type, proto, flags):
socket.gaierror: [Errno -3] Temporary failure in name resolution
```
2022-10-29 03:14:12 +00:00
|
|
|
def _socket_config_str(self) -> str:
|
|
|
|
|
if self.socktype == 'unix':
|
|
|
|
|
return f"<socket type={self.socktype}, path={self.sockpath}>"
|
|
|
|
|
elif self.socktype == 'tcp':
|
|
|
|
|
return f"<socket type={self.socktype}, host={self.host}, port={self.port}>"
|
|
|
|
|
else:
|
|
|
|
|
raise Exception(f"unknown socktype '{self.socktype!r}'")
|
|
|
|
|
|
2020-05-12 10:02:22 +02:00
|
|
|
async def run(self):
|
|
|
|
|
self.runner = web.AppRunner(self.app)
|
|
|
|
|
await self.runner.setup()
|
2021-10-25 12:00:00 +00:00
|
|
|
if self.socktype == 'unix':
|
|
|
|
|
site = web.UnixSite(self.runner, self.sockpath)
|
|
|
|
|
elif self.socktype == 'tcp':
|
|
|
|
|
site = web.TCPSite(self.runner, self.host, self.port)
|
|
|
|
|
else:
|
|
|
|
|
raise Exception(f"unknown socktype '{self.socktype!r}'")
|
daemon: better error msg if rpchost/rpcport is badly configured
old traceback:
```
$ ./run_electrum --testnet -o setconfig rpchost qweasdfcsdf
$ ./run_electrum --testnet -o setconfig rpcport 7777
$ ./run_electrum --testnet daemon
E | daemon.Daemon | taskgroup died.
Traceback (most recent call last):
File "/home/user/wspace/electrum/electrum/daemon.py", line 419, in _run
async with self.taskgroup as group:
File "/home/user/wspace/electrum/packages/aiorpcx/curio.py", line 297, in __aexit__
await self.join()
File "/home/user/wspace/electrum/electrum/util.py", line 1335, in join
task.result()
File "/home/user/wspace/electrum/electrum/daemon.py", line 281, in run
await site.start() #
File "/home/user/wspace/electrum/packages/aiohttp/web_runner.py", line 121, in start
self._server = await loop.create_server(
File "/usr/lib/python3.10/asyncio/base_events.py", line 1471, in create_server
infos = await tasks.gather(*fs)
File "/usr/lib/python3.10/asyncio/base_events.py", line 1408, in _create_server_getaddrinfo
infos = await self._ensure_resolved((host, port), family=family,
File "/usr/lib/python3.10/asyncio/base_events.py", line 1404, in _ensure_resolved
return await loop.getaddrinfo(host, port, family=family, type=type,
File "/usr/lib/python3.10/asyncio/base_events.py", line 860, in getaddrinfo
return await self.run_in_executor(
File "/usr/lib/python3.10/concurrent/futures/thread.py", line 58, in run
result = self.fn(*self.args, **self.kwargs)
File "/usr/lib/python3.10/socket.py", line 955, in getaddrinfo
for res in _socket.getaddrinfo(host, port, family, type, proto, flags):
socket.gaierror: [Errno -3] Temporary failure in name resolution
```
2022-10-29 03:14:12 +00:00
|
|
|
try:
|
|
|
|
|
await site.start()
|
|
|
|
|
except Exception as e:
|
|
|
|
|
raise Exception(f"failed to start CommandsServer at {self._socket_config_str()}. got exc: {e!r}") from None
|
2020-05-12 10:02:22 +02:00
|
|
|
socket = site._server.sockets[0]
|
2021-10-25 12:00:00 +00:00
|
|
|
if self.socktype == 'unix':
|
|
|
|
|
addr = self.sockpath
|
|
|
|
|
elif self.socktype == 'tcp':
|
|
|
|
|
addr = socket.getsockname()
|
|
|
|
|
else:
|
|
|
|
|
raise Exception(f"impossible socktype ({self.socktype!r})")
|
|
|
|
|
os.write(self.fd, bytes(repr((self.socktype, addr, time.time())), 'utf8'))
|
2020-05-12 10:02:22 +02:00
|
|
|
os.close(self.fd)
|
2021-11-15 17:43:34 +01:00
|
|
|
self.logger.info(f"now running and listening. socktype={self.socktype}, addr={addr}")
|
2020-05-12 10:02:22 +02:00
|
|
|
|
|
|
|
|
async def ping(self):
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
async def gui(self, config_options):
|
|
|
|
|
if self.daemon.gui_object:
|
|
|
|
|
if hasattr(self.daemon.gui_object, 'new_window'):
|
|
|
|
|
path = self.config.get_wallet_path(use_gui_last_wallet=True)
|
|
|
|
|
self.daemon.gui_object.new_window(path, config_options.get('url'))
|
|
|
|
|
response = "ok"
|
|
|
|
|
else:
|
|
|
|
|
response = "error: current GUI does not support multiple windows"
|
|
|
|
|
else:
|
|
|
|
|
response = "Error: Electrum is running in daemon mode. Please stop the daemon first."
|
|
|
|
|
return response
|
|
|
|
|
|
|
|
|
|
async def run_cmdline(self, config_options):
|
|
|
|
|
cmdname = config_options['cmd']
|
|
|
|
|
cmd = known_commands[cmdname]
|
|
|
|
|
# arguments passed to function
|
|
|
|
|
args = [config_options.get(x) for x in cmd.params]
|
|
|
|
|
# decode json arguments
|
|
|
|
|
args = [json_decode(i) for i in args]
|
|
|
|
|
# options
|
|
|
|
|
kwargs = {}
|
|
|
|
|
for x in cmd.options:
|
|
|
|
|
kwargs[x] = config_options.get(x)
|
commands: make 'wallet'-mangling in decorator less obscure, and fixes
- some commands expect a 'wallet_path' arg, while others expect 'wallet'
- 'wallet_path' in the end is supposed to be a str,
'wallet' in the end is supposed to be an Optional[Abstract_Wallet]
- initially, in the decorator, 'wallet' can be a str, in which case
the decorator replaces it with an Abstract_Wallet (from the daemon)
- Previously the decorator sometimes converted 'wallet_path' to 'wallet'.
This was because when called from the CLI it was always given 'wallet_path' (and never 'wallet),
while when called from JSON-RPC it was given either 'wallet' or 'wallet_path' (depending on command).
Now, the CLI also behaves as JSON-RPC, and hence 'wallet_path' and 'wallet' are fully separate.
- A bug is fixed where, when a command that only optionally takes a 'wallet' (such as gettransaction),
was called from the JSON-RPC with the arg present, it raised; and when called from CLI with the arg present
the arg was not actually passed to the command.
- A bug is fixed where if one command calls another command (that both take a 'wallet'),
it would raise (due to assuming 'wallet' is str and needs to be converted to Abstract_Wallet).
This fixes #6154.
-----
$ ./run_electrum --testnet daemon -d
$ ./run_electrum --testnet load_wallet -w ~/.electrum/testnet/wallets/default_wallet
$ curl --data-binary '{"id":"curltext","jsonrpc":"2.0","method":"gettransaction","params":{"txid":"9f43ff71ea2594873e4e7d15e61254a3661ff2df1af76325c854d9aa199550ce"}}' http://user:pass@127.0.0.1:7777
{"jsonrpc": "2.0", "result": "0200000001caaac6b5eb916e3067d0224f942fb331ce1dcfb4031cfb479e7941dcf95e409801000000fdfe0000483045022100e2a508bb78c2172eb03f081a342454ba1d24669e959700973b1a742a4fedd0c302203174e06feda265031cf9aa0364d4a4eafb71b0c0a62e76be7795cfbb307b677a01483045022100d0e14564838fac754395158741d64c73da2b86e7900dfdc6a63c7492b232ba130220778e7e7c21d94ebcd340057302aeff7e9a797a3aa3e0ac4884e9ff27339ea6e9014c69522102091f0b4d8ab30016a5d1c088249e02883fad8160f06fa53588ad8598650a3e6221035f2f8263bb3608d6cc4ee03bd4cb8d65c4d70af71049f05fbfee4978832a1fd22103fe42dab58718ea0413f7c8de693cdeee22ce19b1dc34c0bbdd7a48245465c5a253aefdffffff01cb9f0700000000001976a914c13fd6294d1be7b9410a5538f4b4ef10fc594ee788ac802c1800", "id": "curltext"}
$ curl --data-binary '{"id":"curltext","jsonrpc":"2.0","method":"gettransaction","params":{"txid":"9f43ff71ea2594873e4e7d15e61254a3661ff2df1af76325c854d9aa199550ce", "wallet":"~/.electrum/testnet/wallets/default_wallet"}}' http://user:pass@127.0.0.1:7777
{"jsonrpc": "2.0", "error": {"code": -32000, "message": "'str' object has no attribute 'db'"}, "id": "curltext"}
2020-05-14 16:33:02 +02:00
|
|
|
if 'wallet_path' in cmd.options:
|
2020-05-12 10:02:22 +02:00
|
|
|
kwargs['wallet_path'] = config_options.get('wallet_path')
|
commands: make 'wallet'-mangling in decorator less obscure, and fixes
- some commands expect a 'wallet_path' arg, while others expect 'wallet'
- 'wallet_path' in the end is supposed to be a str,
'wallet' in the end is supposed to be an Optional[Abstract_Wallet]
- initially, in the decorator, 'wallet' can be a str, in which case
the decorator replaces it with an Abstract_Wallet (from the daemon)
- Previously the decorator sometimes converted 'wallet_path' to 'wallet'.
This was because when called from the CLI it was always given 'wallet_path' (and never 'wallet),
while when called from JSON-RPC it was given either 'wallet' or 'wallet_path' (depending on command).
Now, the CLI also behaves as JSON-RPC, and hence 'wallet_path' and 'wallet' are fully separate.
- A bug is fixed where, when a command that only optionally takes a 'wallet' (such as gettransaction),
was called from the JSON-RPC with the arg present, it raised; and when called from CLI with the arg present
the arg was not actually passed to the command.
- A bug is fixed where if one command calls another command (that both take a 'wallet'),
it would raise (due to assuming 'wallet' is str and needs to be converted to Abstract_Wallet).
This fixes #6154.
-----
$ ./run_electrum --testnet daemon -d
$ ./run_electrum --testnet load_wallet -w ~/.electrum/testnet/wallets/default_wallet
$ curl --data-binary '{"id":"curltext","jsonrpc":"2.0","method":"gettransaction","params":{"txid":"9f43ff71ea2594873e4e7d15e61254a3661ff2df1af76325c854d9aa199550ce"}}' http://user:pass@127.0.0.1:7777
{"jsonrpc": "2.0", "result": "0200000001caaac6b5eb916e3067d0224f942fb331ce1dcfb4031cfb479e7941dcf95e409801000000fdfe0000483045022100e2a508bb78c2172eb03f081a342454ba1d24669e959700973b1a742a4fedd0c302203174e06feda265031cf9aa0364d4a4eafb71b0c0a62e76be7795cfbb307b677a01483045022100d0e14564838fac754395158741d64c73da2b86e7900dfdc6a63c7492b232ba130220778e7e7c21d94ebcd340057302aeff7e9a797a3aa3e0ac4884e9ff27339ea6e9014c69522102091f0b4d8ab30016a5d1c088249e02883fad8160f06fa53588ad8598650a3e6221035f2f8263bb3608d6cc4ee03bd4cb8d65c4d70af71049f05fbfee4978832a1fd22103fe42dab58718ea0413f7c8de693cdeee22ce19b1dc34c0bbdd7a48245465c5a253aefdffffff01cb9f0700000000001976a914c13fd6294d1be7b9410a5538f4b4ef10fc594ee788ac802c1800", "id": "curltext"}
$ curl --data-binary '{"id":"curltext","jsonrpc":"2.0","method":"gettransaction","params":{"txid":"9f43ff71ea2594873e4e7d15e61254a3661ff2df1af76325c854d9aa199550ce", "wallet":"~/.electrum/testnet/wallets/default_wallet"}}' http://user:pass@127.0.0.1:7777
{"jsonrpc": "2.0", "error": {"code": -32000, "message": "'str' object has no attribute 'db'"}, "id": "curltext"}
2020-05-14 16:33:02 +02:00
|
|
|
elif 'wallet' in cmd.options:
|
|
|
|
|
kwargs['wallet'] = config_options.get('wallet_path')
|
2020-05-12 10:02:22 +02:00
|
|
|
func = getattr(self.cmd_runner, cmd.name)
|
|
|
|
|
# fixme: not sure how to retrieve message in jsonrpcclient
|
|
|
|
|
try:
|
|
|
|
|
result = await func(*args, **kwargs)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
result = {'error':str(e)}
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class WatchTowerServer(AuthenticatedServer):
|
|
|
|
|
|
|
|
|
|
def __init__(self, network, netaddress):
|
2020-05-10 14:52:20 +02:00
|
|
|
self.addr = netaddress
|
2019-07-05 14:42:09 +02:00
|
|
|
self.config = network.config
|
2019-07-30 11:47:17 +02:00
|
|
|
self.network = network
|
2020-05-12 10:02:22 +02:00
|
|
|
watchtower_user = self.config.get('watchtower_user', '')
|
|
|
|
|
watchtower_password = self.config.get('watchtower_password', '')
|
|
|
|
|
AuthenticatedServer.__init__(self, watchtower_user, watchtower_password)
|
2019-07-05 14:42:09 +02:00
|
|
|
self.lnwatcher = network.local_watchtower
|
2019-07-30 11:47:17 +02:00
|
|
|
self.app = web.Application()
|
|
|
|
|
self.app.router.add_post("/", self.handle)
|
2020-06-09 17:45:04 +02:00
|
|
|
self.register_method(self.get_ctn)
|
|
|
|
|
self.register_method(self.add_sweep_tx)
|
2019-07-30 11:47:17 +02:00
|
|
|
|
|
|
|
|
async def run(self):
|
|
|
|
|
self.runner = web.AppRunner(self.app)
|
|
|
|
|
await self.runner.setup()
|
2020-05-10 14:52:20 +02:00
|
|
|
site = web.TCPSite(self.runner, host=str(self.addr.host), port=self.addr.port, ssl_context=self.config.get_ssl_context())
|
2019-07-30 11:47:17 +02:00
|
|
|
await site.start()
|
2021-11-15 17:43:34 +01:00
|
|
|
self.logger.info(f"now running and listening. addr={self.addr}")
|
2019-07-30 11:47:17 +02:00
|
|
|
|
|
|
|
|
async def get_ctn(self, *args):
|
2021-09-27 10:31:44 +02:00
|
|
|
return await self.lnwatcher.get_ctn(*args)
|
2019-07-30 11:47:17 +02:00
|
|
|
|
|
|
|
|
async def add_sweep_tx(self, *args):
|
|
|
|
|
return await self.lnwatcher.sweepstore.add_sweep_tx(*args)
|
|
|
|
|
|
2019-09-13 12:26:27 +02:00
|
|
|
|
2019-09-03 14:44:33 +02:00
|
|
|
|
2019-11-21 15:12:14 +01:00
|
|
|
|
2019-08-15 13:17:16 +02:00
|
|
|
class Daemon(Logger):
|
2015-11-30 10:09:54 +01:00
|
|
|
|
2020-04-14 16:56:17 +02:00
|
|
|
network: Optional[Network]
|
2021-11-05 20:21:50 +01:00
|
|
|
gui_object: Optional['gui.BaseElectrumGui']
|
2020-04-14 16:56:17 +02:00
|
|
|
|
2018-11-16 14:39:22 +01:00
|
|
|
@profiler
|
2018-10-22 16:41:25 +02:00
|
|
|
def __init__(self, config: SimpleConfig, fd=None, *, listen_jsonrpc=True):
|
2019-08-15 13:17:16 +02:00
|
|
|
Logger.__init__(self)
|
2015-11-30 10:09:54 +01:00
|
|
|
self.config = config
|
2021-07-07 16:57:09 +03:00
|
|
|
self.listen_jsonrpc = listen_jsonrpc
|
2018-10-01 17:56:51 +02:00
|
|
|
if fd is None and listen_jsonrpc:
|
2019-08-15 09:58:23 +02:00
|
|
|
fd = get_file_descriptor(config)
|
|
|
|
|
if fd is None:
|
|
|
|
|
raise Exception('failed to lock daemon; already running?')
|
2020-10-05 18:02:37 +02:00
|
|
|
if 'wallet_path' in config.cmdline_options:
|
|
|
|
|
self.logger.warning("Ignoring parameter 'wallet_path' for daemon. "
|
|
|
|
|
"Use the load_wallet command instead.")
|
asyncio: stop using get_event_loop(). introduce ~singleton loop.
asyncio.get_event_loop() became deprecated in python3.10. (see https://github.com/python/cpython/issues/83710)
```
.../electrum/electrum/daemon.py:470: DeprecationWarning: There is no current event loop
self.asyncio_loop = asyncio.get_event_loop()
.../electrum/electrum/network.py:276: DeprecationWarning: There is no current event loop
self.asyncio_loop = asyncio.get_event_loop()
```
Also, according to that thread, "set_event_loop() [... is] not deprecated by oversight".
So, we stop using get_event_loop() and set_event_loop() in our own code.
Note that libraries we use (such as the stdlib for python <3.10), might call get_event_loop,
which then relies on us having called set_event_loop e.g. for the GUI thread. To work around
this, a custom event loop policy providing a get_event_loop implementation is used.
Previously, we have been using a single asyncio event loop, created with
util.create_and_start_event_loop, and code in many places got a reference to this loop
using asyncio.get_event_loop().
Now, we still use a single asyncio event loop, but it is now stored as a global in
util._asyncio_event_loop (access with util.get_asyncio_loop()).
I believe these changes also fix https://github.com/spesmilo/electrum/issues/5376
2022-04-29 18:24:49 +02:00
|
|
|
self.asyncio_loop = util.get_asyncio_loop()
|
2020-01-09 17:50:05 +01:00
|
|
|
self.network = None
|
|
|
|
|
if not config.get('offline'):
|
|
|
|
|
self.network = Network(config, daemon=self)
|
2018-02-14 10:40:11 +01:00
|
|
|
self.fx = FxThread(config, self.network)
|
2019-08-15 13:17:16 +02:00
|
|
|
self.gui_object = None
|
2019-03-04 02:49:41 +01:00
|
|
|
# path -> wallet; make sure path is standardized.
|
2019-09-09 22:15:11 +02:00
|
|
|
self._wallets = {} # type: Dict[str, Abstract_Wallet]
|
2022-07-06 18:33:08 +02:00
|
|
|
self._wallet_lock = threading.RLock()
|
2020-01-09 17:50:05 +01:00
|
|
|
daemon_jobs = []
|
2020-05-12 10:02:22 +02:00
|
|
|
# Setup commands server
|
|
|
|
|
self.commands_server = None
|
2018-10-01 17:56:51 +02:00
|
|
|
if listen_jsonrpc:
|
2020-05-12 10:02:22 +02:00
|
|
|
self.commands_server = CommandsServer(self, fd)
|
|
|
|
|
daemon_jobs.append(self.commands_server.run())
|
2019-09-04 13:07:44 +02:00
|
|
|
# server-side watchtower
|
2020-01-09 17:50:05 +01:00
|
|
|
self.watchtower = None
|
2020-05-10 14:52:20 +02:00
|
|
|
watchtower_address = self.config.get_netaddress('watchtower_address')
|
|
|
|
|
if not config.get('offline') and watchtower_address:
|
|
|
|
|
self.watchtower = WatchTowerServer(self.network, watchtower_address)
|
2020-01-09 17:50:05 +01:00
|
|
|
daemon_jobs.append(self.watchtower.run)
|
2018-10-13 14:12:30 +02:00
|
|
|
if self.network:
|
2020-01-09 17:50:05 +01:00
|
|
|
self.network.start(jobs=[self.fx.run])
|
2020-10-08 06:36:02 +02:00
|
|
|
# prepare lightning functionality, also load channel db early
|
2020-11-11 11:03:31 +01:00
|
|
|
if self.config.get('use_gossip', False):
|
|
|
|
|
self.network.start_gossip()
|
2020-01-09 17:50:05 +01:00
|
|
|
|
2021-11-05 18:33:03 +01:00
|
|
|
self._stop_entered = False
|
|
|
|
|
self._stopping_soon_or_errored = threading.Event()
|
2021-11-05 17:24:03 +01:00
|
|
|
self._stopped_event = threading.Event()
|
2022-02-08 12:34:49 +01:00
|
|
|
self.taskgroup = OldTaskGroup()
|
2020-01-09 17:50:05 +01:00
|
|
|
asyncio.run_coroutine_threadsafe(self._run(jobs=daemon_jobs), self.asyncio_loop)
|
|
|
|
|
|
|
|
|
|
@log_exceptions
|
|
|
|
|
async def _run(self, jobs: Iterable = None):
|
|
|
|
|
if jobs is None:
|
|
|
|
|
jobs = []
|
2020-02-27 20:22:49 +01:00
|
|
|
self.logger.info("starting taskgroup.")
|
2020-01-09 17:50:05 +01:00
|
|
|
try:
|
|
|
|
|
async with self.taskgroup as group:
|
|
|
|
|
[await group.spawn(job) for job in jobs]
|
2020-01-11 00:04:00 +01:00
|
|
|
await group.spawn(asyncio.Event().wait) # run forever (until cancel)
|
2020-02-27 20:22:49 +01:00
|
|
|
except Exception as e:
|
|
|
|
|
self.logger.exception("taskgroup died.")
|
2021-11-05 18:33:03 +01:00
|
|
|
util.send_exception_to_crash_reporter(e)
|
2020-01-09 17:50:05 +01:00
|
|
|
finally:
|
2020-02-27 20:22:49 +01:00
|
|
|
self.logger.info("taskgroup stopped.")
|
2021-11-05 18:33:03 +01:00
|
|
|
# note: we could just "await self.stop()", but in that case GUI users would
|
|
|
|
|
# not see the exception (especially if the GUI did not start yet).
|
|
|
|
|
self._stopping_soon_or_errored.set()
|
2016-08-15 08:14:19 +02:00
|
|
|
|
2022-07-06 18:33:08 +02:00
|
|
|
def with_wallet_lock(func):
|
|
|
|
|
def func_wrapper(self: 'Daemon', *args, **kwargs):
|
|
|
|
|
with self._wallet_lock:
|
|
|
|
|
return func(self, *args, **kwargs)
|
|
|
|
|
return func_wrapper
|
|
|
|
|
|
|
|
|
|
@with_wallet_lock
|
2019-11-22 15:54:34 +01:00
|
|
|
def load_wallet(self, path, password, *, manual_upgrades=True) -> Optional[Abstract_Wallet]:
|
2019-02-08 12:59:06 +01:00
|
|
|
path = standardize_path(path)
|
2016-07-02 08:58:56 +02:00
|
|
|
# wizard will be launched if we return
|
2019-09-09 22:15:11 +02:00
|
|
|
if path in self._wallets:
|
|
|
|
|
wallet = self._wallets[path]
|
2016-06-20 16:25:11 +02:00
|
|
|
return wallet
|
2022-07-06 18:33:08 +02:00
|
|
|
wallet = self._load_wallet(path, password, manual_upgrades=manual_upgrades, config=self.config)
|
|
|
|
|
if wallet is None:
|
|
|
|
|
return
|
|
|
|
|
wallet.start_network(self.network)
|
|
|
|
|
self._wallets[path] = wallet
|
2022-10-28 11:55:27 +02:00
|
|
|
run_hook('daemon_wallet_loaded', self, wallet)
|
2022-07-06 18:33:08 +02:00
|
|
|
return wallet
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def _load_wallet(
|
|
|
|
|
path,
|
|
|
|
|
password,
|
|
|
|
|
*,
|
|
|
|
|
manual_upgrades: bool = True,
|
|
|
|
|
config: SimpleConfig,
|
|
|
|
|
) -> Optional[Abstract_Wallet]:
|
|
|
|
|
path = standardize_path(path)
|
2020-02-05 15:13:37 +01:00
|
|
|
storage = WalletStorage(path)
|
2017-03-06 08:33:35 +01:00
|
|
|
if not storage.file_exists():
|
2016-06-20 16:25:11 +02:00
|
|
|
return
|
2017-02-09 17:08:27 +01:00
|
|
|
if storage.is_encrypted():
|
|
|
|
|
if not password:
|
2017-03-08 11:56:01 +01:00
|
|
|
return
|
2017-03-06 08:33:35 +01:00
|
|
|
storage.decrypt(password)
|
2020-02-05 15:13:37 +01:00
|
|
|
# read data, pass it to db
|
|
|
|
|
db = WalletDB(storage.read(), manual_upgrades=manual_upgrades)
|
|
|
|
|
if db.requires_split():
|
2016-06-20 16:25:11 +02:00
|
|
|
return
|
2020-02-05 15:13:37 +01:00
|
|
|
if db.requires_upgrade():
|
2019-03-04 17:23:43 +01:00
|
|
|
return
|
2020-02-05 15:13:37 +01:00
|
|
|
if db.get_action():
|
2016-10-02 12:30:57 +02:00
|
|
|
return
|
2022-07-06 18:33:08 +02:00
|
|
|
wallet = Wallet(db, storage, config=config)
|
2015-11-30 10:09:54 +01:00
|
|
|
return wallet
|
|
|
|
|
|
2022-07-06 18:33:08 +02:00
|
|
|
@with_wallet_lock
|
2019-09-09 22:15:11 +02:00
|
|
|
def add_wallet(self, wallet: Abstract_Wallet) -> None:
|
2016-06-20 16:25:11 +02:00
|
|
|
path = wallet.storage.path
|
2019-03-04 02:49:41 +01:00
|
|
|
path = standardize_path(path)
|
2019-09-09 22:15:11 +02:00
|
|
|
self._wallets[path] = wallet
|
2016-06-20 16:25:11 +02:00
|
|
|
|
commands: make 'wallet'-mangling in decorator less obscure, and fixes
- some commands expect a 'wallet_path' arg, while others expect 'wallet'
- 'wallet_path' in the end is supposed to be a str,
'wallet' in the end is supposed to be an Optional[Abstract_Wallet]
- initially, in the decorator, 'wallet' can be a str, in which case
the decorator replaces it with an Abstract_Wallet (from the daemon)
- Previously the decorator sometimes converted 'wallet_path' to 'wallet'.
This was because when called from the CLI it was always given 'wallet_path' (and never 'wallet),
while when called from JSON-RPC it was given either 'wallet' or 'wallet_path' (depending on command).
Now, the CLI also behaves as JSON-RPC, and hence 'wallet_path' and 'wallet' are fully separate.
- A bug is fixed where, when a command that only optionally takes a 'wallet' (such as gettransaction),
was called from the JSON-RPC with the arg present, it raised; and when called from CLI with the arg present
the arg was not actually passed to the command.
- A bug is fixed where if one command calls another command (that both take a 'wallet'),
it would raise (due to assuming 'wallet' is str and needs to be converted to Abstract_Wallet).
This fixes #6154.
-----
$ ./run_electrum --testnet daemon -d
$ ./run_electrum --testnet load_wallet -w ~/.electrum/testnet/wallets/default_wallet
$ curl --data-binary '{"id":"curltext","jsonrpc":"2.0","method":"gettransaction","params":{"txid":"9f43ff71ea2594873e4e7d15e61254a3661ff2df1af76325c854d9aa199550ce"}}' http://user:pass@127.0.0.1:7777
{"jsonrpc": "2.0", "result": "0200000001caaac6b5eb916e3067d0224f942fb331ce1dcfb4031cfb479e7941dcf95e409801000000fdfe0000483045022100e2a508bb78c2172eb03f081a342454ba1d24669e959700973b1a742a4fedd0c302203174e06feda265031cf9aa0364d4a4eafb71b0c0a62e76be7795cfbb307b677a01483045022100d0e14564838fac754395158741d64c73da2b86e7900dfdc6a63c7492b232ba130220778e7e7c21d94ebcd340057302aeff7e9a797a3aa3e0ac4884e9ff27339ea6e9014c69522102091f0b4d8ab30016a5d1c088249e02883fad8160f06fa53588ad8598650a3e6221035f2f8263bb3608d6cc4ee03bd4cb8d65c4d70af71049f05fbfee4978832a1fd22103fe42dab58718ea0413f7c8de693cdeee22ce19b1dc34c0bbdd7a48245465c5a253aefdffffff01cb9f0700000000001976a914c13fd6294d1be7b9410a5538f4b4ef10fc594ee788ac802c1800", "id": "curltext"}
$ curl --data-binary '{"id":"curltext","jsonrpc":"2.0","method":"gettransaction","params":{"txid":"9f43ff71ea2594873e4e7d15e61254a3661ff2df1af76325c854d9aa199550ce", "wallet":"~/.electrum/testnet/wallets/default_wallet"}}' http://user:pass@127.0.0.1:7777
{"jsonrpc": "2.0", "error": {"code": -32000, "message": "'str' object has no attribute 'db'"}, "id": "curltext"}
2020-05-14 16:33:02 +02:00
|
|
|
def get_wallet(self, path: str) -> Optional[Abstract_Wallet]:
|
2019-03-04 02:49:41 +01:00
|
|
|
path = standardize_path(path)
|
2019-09-09 22:15:11 +02:00
|
|
|
return self._wallets.get(path)
|
2017-03-05 13:30:57 +01:00
|
|
|
|
2022-07-06 18:33:08 +02:00
|
|
|
@with_wallet_lock
|
2019-09-09 22:15:11 +02:00
|
|
|
def get_wallets(self) -> Dict[str, Abstract_Wallet]:
|
|
|
|
|
return dict(self._wallets) # copy
|
|
|
|
|
|
|
|
|
|
def delete_wallet(self, path: str) -> bool:
|
2019-02-08 11:17:48 +01:00
|
|
|
self.stop_wallet(path)
|
|
|
|
|
if os.path.exists(path):
|
|
|
|
|
os.unlink(path)
|
|
|
|
|
return True
|
|
|
|
|
return False
|
|
|
|
|
|
2019-09-09 22:15:11 +02:00
|
|
|
def stop_wallet(self, path: str) -> bool:
|
2021-06-17 12:35:31 +02:00
|
|
|
"""Returns True iff a wallet was found."""
|
|
|
|
|
# note: this must not be called from the event loop. # TODO raise if so
|
|
|
|
|
fut = asyncio.run_coroutine_threadsafe(self._stop_wallet(path), self.asyncio_loop)
|
|
|
|
|
return fut.result()
|
|
|
|
|
|
2022-07-06 18:33:08 +02:00
|
|
|
@with_wallet_lock
|
2021-06-17 12:35:31 +02:00
|
|
|
async def _stop_wallet(self, path: str) -> bool:
|
2019-09-04 20:16:47 +02:00
|
|
|
"""Returns True iff a wallet was found."""
|
2019-03-04 02:49:41 +01:00
|
|
|
path = standardize_path(path)
|
2019-09-09 22:15:11 +02:00
|
|
|
wallet = self._wallets.pop(path, None)
|
2019-09-04 20:16:47 +02:00
|
|
|
if not wallet:
|
|
|
|
|
return False
|
2021-06-17 12:35:31 +02:00
|
|
|
await wallet.stop()
|
2019-09-04 20:16:47 +02:00
|
|
|
return True
|
2016-06-20 16:25:11 +02:00
|
|
|
|
2019-08-15 13:17:16 +02:00
|
|
|
def run_daemon(self):
|
|
|
|
|
try:
|
2021-11-05 18:33:03 +01:00
|
|
|
self._stopping_soon_or_errored.wait()
|
2019-08-15 13:17:16 +02:00
|
|
|
except KeyboardInterrupt:
|
2022-10-29 02:58:09 +00:00
|
|
|
self.logger.info("got KeyboardInterrupt")
|
|
|
|
|
# we either initiate shutdown now,
|
|
|
|
|
# or it has already been initiated (in which case this is a no-op):
|
|
|
|
|
self.logger.info("run_daemon is calling stop()")
|
|
|
|
|
asyncio.run_coroutine_threadsafe(self.stop(), self.asyncio_loop).result()
|
|
|
|
|
# wait until "stop" finishes:
|
2021-11-05 17:24:03 +01:00
|
|
|
self._stopped_event.wait()
|
2019-08-15 13:17:16 +02:00
|
|
|
|
2021-03-17 19:16:07 +01:00
|
|
|
async def stop(self):
|
2021-11-05 18:33:03 +01:00
|
|
|
if self._stop_entered:
|
2021-11-05 17:24:03 +01:00
|
|
|
return
|
2021-11-05 18:33:03 +01:00
|
|
|
self._stop_entered = True
|
|
|
|
|
self._stopping_soon_or_errored.set()
|
2021-11-05 17:24:03 +01:00
|
|
|
self.logger.info("stop() entered. initiating shutdown")
|
2021-03-17 19:16:07 +01:00
|
|
|
try:
|
|
|
|
|
if self.gui_object:
|
|
|
|
|
self.gui_object.stop()
|
2021-11-05 17:24:03 +01:00
|
|
|
self.logger.info("stopping all wallets")
|
2022-02-08 12:34:49 +01:00
|
|
|
async with OldTaskGroup() as group:
|
2021-11-05 17:24:03 +01:00
|
|
|
for k, wallet in self._wallets.items():
|
|
|
|
|
await group.spawn(wallet.stop())
|
|
|
|
|
self.logger.info("stopping network and taskgroup")
|
|
|
|
|
async with ignore_after(2):
|
2022-02-08 12:34:49 +01:00
|
|
|
async with OldTaskGroup() as group:
|
2021-11-05 17:24:03 +01:00
|
|
|
if self.network:
|
|
|
|
|
await group.spawn(self.network.stop(full_shutdown=True))
|
|
|
|
|
await group.spawn(self.taskgroup.cancel_remaining())
|
2021-03-17 19:16:07 +01:00
|
|
|
finally:
|
2021-07-07 16:57:09 +03:00
|
|
|
if self.listen_jsonrpc:
|
|
|
|
|
self.logger.info("removing lockfile")
|
|
|
|
|
remove_lockfile(get_lockfile(self.config))
|
2021-03-17 19:16:07 +01:00
|
|
|
self.logger.info("stopped")
|
2021-11-05 17:24:03 +01:00
|
|
|
self._stopped_event.set()
|
2016-01-31 11:43:11 +09:00
|
|
|
|
2019-08-15 13:17:16 +02:00
|
|
|
def run_gui(self, config, plugins):
|
2021-11-09 01:02:57 +01:00
|
|
|
threading.current_thread().name = 'GUI'
|
2016-01-31 11:43:11 +09:00
|
|
|
gui_name = config.get('gui', 'qt')
|
|
|
|
|
if gui_name in ['lite', 'classic']:
|
|
|
|
|
gui_name = 'qt'
|
2019-12-10 23:31:58 +01:00
|
|
|
self.logger.info(f'launching GUI: {gui_name}')
|
2018-02-15 17:30:40 +01:00
|
|
|
try:
|
2022-04-11 16:53:25 +02:00
|
|
|
try:
|
|
|
|
|
gui = __import__('electrum.gui.' + gui_name, fromlist=['electrum'])
|
|
|
|
|
except GuiImportError as e:
|
|
|
|
|
sys.exit(str(e))
|
2021-11-05 20:21:50 +01:00
|
|
|
self.gui_object = gui.ElectrumGui(config=config, daemon=self, plugins=plugins)
|
2021-11-05 18:33:03 +01:00
|
|
|
if not self._stop_entered:
|
2021-11-05 17:24:03 +01:00
|
|
|
self.gui_object.main()
|
|
|
|
|
else:
|
|
|
|
|
# If daemon.stop() was called before gui_object got created, stop gui now.
|
|
|
|
|
self.gui_object.stop()
|
2018-02-15 17:30:40 +01:00
|
|
|
except BaseException as e:
|
2020-02-19 15:45:36 +01:00
|
|
|
self.logger.error(f'GUI raised exception: {repr(e)}. shutting down.')
|
|
|
|
|
raise
|
|
|
|
|
finally:
|
2018-02-15 17:30:40 +01:00
|
|
|
# app will exit now
|
2021-11-05 17:24:03 +01:00
|
|
|
asyncio.run_coroutine_threadsafe(self.stop(), self.asyncio_loop).result()
|
2022-07-06 18:33:08 +02:00
|
|
|
|
|
|
|
|
@with_wallet_lock
|
|
|
|
|
def _check_password_for_directory(self, *, old_password, new_password=None, wallet_dir: str) -> Tuple[bool, bool]:
|
|
|
|
|
"""Checks password against all wallets (in dir), returns whether they can be unified and whether they are already.
|
|
|
|
|
If new_password is not None, update all wallet passwords to new_password.
|
|
|
|
|
"""
|
|
|
|
|
assert os.path.exists(wallet_dir), f"path {wallet_dir!r} does not exist"
|
|
|
|
|
failed = []
|
|
|
|
|
is_unified = True
|
|
|
|
|
for filename in os.listdir(wallet_dir):
|
|
|
|
|
path = os.path.join(wallet_dir, filename)
|
|
|
|
|
path = standardize_path(path)
|
|
|
|
|
if not os.path.isfile(path):
|
|
|
|
|
continue
|
|
|
|
|
wallet = self.get_wallet(path)
|
|
|
|
|
if wallet is None:
|
|
|
|
|
try:
|
|
|
|
|
wallet = self._load_wallet(path, old_password, manual_upgrades=False, config=self.config)
|
|
|
|
|
except util.InvalidPassword:
|
|
|
|
|
pass
|
|
|
|
|
except Exception:
|
|
|
|
|
self.logger.exception(f'failed to load wallet at {path!r}:')
|
|
|
|
|
pass
|
|
|
|
|
if wallet is None:
|
|
|
|
|
failed.append(path)
|
|
|
|
|
continue
|
|
|
|
|
if not wallet.storage.is_encrypted():
|
|
|
|
|
is_unified = False
|
|
|
|
|
try:
|
|
|
|
|
wallet.check_password(old_password)
|
|
|
|
|
except Exception:
|
|
|
|
|
failed.append(path)
|
|
|
|
|
continue
|
|
|
|
|
if new_password:
|
|
|
|
|
self.logger.info(f'updating password for wallet: {path!r}')
|
|
|
|
|
wallet.update_password(old_password, new_password, encrypt_storage=True)
|
|
|
|
|
can_be_unified = failed == []
|
|
|
|
|
is_unified = can_be_unified and is_unified
|
|
|
|
|
return can_be_unified, is_unified
|
|
|
|
|
|
|
|
|
|
@with_wallet_lock
|
|
|
|
|
def update_password_for_directory(
|
|
|
|
|
self,
|
|
|
|
|
*,
|
|
|
|
|
old_password,
|
|
|
|
|
new_password,
|
|
|
|
|
wallet_dir: Optional[str] = None,
|
|
|
|
|
) -> bool:
|
|
|
|
|
"""returns whether password is unified"""
|
|
|
|
|
if new_password is None:
|
|
|
|
|
# we opened a non-encrypted wallet
|
|
|
|
|
return False
|
|
|
|
|
if wallet_dir is None:
|
|
|
|
|
wallet_dir = os.path.dirname(self.config.get_wallet_path())
|
|
|
|
|
can_be_unified, is_unified = self._check_password_for_directory(
|
|
|
|
|
old_password=old_password, new_password=None, wallet_dir=wallet_dir)
|
|
|
|
|
if not can_be_unified:
|
|
|
|
|
return False
|
|
|
|
|
if is_unified and old_password == new_password:
|
|
|
|
|
return True
|
|
|
|
|
self._check_password_for_directory(
|
|
|
|
|
old_password=old_password, new_password=new_password, wallet_dir=wallet_dir)
|
|
|
|
|
return True
|