URI: 
       tMerge pull request #6220 from spesmilo/jsonrpc_nodeps - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit d9c525801420d07b3597295bfb43003ff92811bd
   DIR parent 35093434473ef4331c93d49d4220f0c56922b260
  HTML Author: ThomasV <thomasv@electrum.org>
       Date:   Tue,  9 Jun 2020 19:37:31 +0200
       
       Merge pull request #6220 from spesmilo/jsonrpc_nodeps
       
       Remove dependencies: jsonrpcserver, jsonrpcclient
       Diffstat:
         M contrib/build-wine/deterministic.s… |       2 --
         M contrib/osx/osx.spec                |       2 --
         M contrib/requirements/requirements.… |       2 --
         M electrum/daemon.py                  |      64 ++++++++++++++++++-------------
         M electrum/lnworker.py                |      13 +++++--------
         M electrum/util.py                    |      30 ++++++++++++++++++++++++++++++
       
       6 files changed, 72 insertions(+), 41 deletions(-)
       ---
   DIR diff --git a/contrib/build-wine/deterministic.spec b/contrib/build-wine/deterministic.spec
       t@@ -50,8 +50,6 @@ datas += collect_data_files('btchip')
        datas += collect_data_files('keepkeylib')
        datas += collect_data_files('ckcc')
        datas += collect_data_files('bitbox02')
       -datas += collect_data_files('jsonrpcserver')
       -datas += collect_data_files('jsonrpcclient')
        
        # We don't put these files in to actually include them in the script but to make the Analysis method scan them for imports
        a = Analysis([home+'run_electrum',
   DIR diff --git a/contrib/osx/osx.spec b/contrib/osx/osx.spec
       t@@ -83,8 +83,6 @@ datas += collect_data_files('btchip')
        datas += collect_data_files('keepkeylib')
        datas += collect_data_files('ckcc')
        datas += collect_data_files('bitbox02')
       -datas += collect_data_files('jsonrpcserver')
       -datas += collect_data_files('jsonrpcclient')
        
        # Add the QR Scanner helper app
        datas += [(electrum + "contrib/osx/CalinsQRReader/build/Release/CalinsQRReader.app", "./contrib/osx/CalinsQRReader/build/Release/CalinsQRReader.app")]
   DIR diff --git a/contrib/requirements/requirements.txt b/contrib/requirements/requirements.txt
       t@@ -9,6 +9,4 @@ aiohttp>=3.3.0,<4.0.0
        aiohttp_socks>=0.3
        certifi
        bitstring
       -jsonrpcserver
       -jsonrpcclient
        attrs
   DIR diff --git a/electrum/daemon.py b/electrum/daemon.py
       t@@ -29,18 +29,15 @@ import time
        import traceback
        import sys
        import threading
       -from typing import Dict, Optional, Tuple, Iterable
       +from typing import Dict, Optional, Tuple, Iterable, Callable, Union, Sequence, Mapping
        from base64 import b64decode, b64encode
        from collections import defaultdict
        import concurrent
        from concurrent import futures
       +import json
        
        import aiohttp
        from aiohttp import web, client_exceptions
       -import jsonrpcclient
       -import jsonrpcserver
       -from jsonrpcserver import response
       -from jsonrpcclient.clients.aiohttp_client import AiohttpClient
        from aiorpcx import TaskGroup
        
        from . import util
       t@@ -107,10 +104,8 @@ def request(config: SimpleConfig, endpoint, args=(), timeout=60):
                loop = asyncio.get_event_loop()
                async def request_coroutine():
                    async with aiohttp.ClientSession(auth=auth) as session:
       -                server = AiohttpClient(session, server_url, timeout=timeout)
       -                f = getattr(server, endpoint)
       -                response = await f(*args)
       -                return response.data.result
       +                c = util.JsonRPCClient(session, server_url)
       +                return await c.request(endpoint, *args)
                try:
                    fut = asyncio.run_coroutine_threadsafe(request_coroutine(), loop)
                    return fut.result(timeout=timeout)
       t@@ -156,6 +151,11 @@ class AuthenticatedServer(Logger):
                self.rpc_user = rpc_user
                self.rpc_password = rpc_password
                self.auth_lock = asyncio.Lock()
       +        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
        
            async def authenticate(self, headers):
                if self.rpc_password == '':
       t@@ -184,16 +184,28 @@ class AuthenticatedServer(Logger):
                                            text='Unauthorized', status=401)
                    except AuthenticationCredentialsInvalid:
                        return web.Response(text='Forbidden', status=403)
       -        request = await request.text()
       -        response = await jsonrpcserver.async_dispatch(request, methods=self.methods)
       -        if isinstance(response, jsonrpcserver.response.ExceptionResponse):
       -            self.logger.error(f"error handling request: {request}", exc_info=response.exc)
       -            # this exposes the error message to the client
       -            response.message = str(response.exc)
       -        if response.wanted:
       -            return web.json_response(response.deserialized(), status=response.http_status)
       -        else:
       -            return web.Response()
       +        try:
       +            request = await request.text()
       +            request = json.loads(request)
       +            method = request['method']
       +            _id = request['id']
       +            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")
       +            return web.Response(text='Invalid Request', status=500)
       +        response = {'id': _id}
       +        try:
       +            if isinstance(params, dict):
       +                response['result'] = await f(**params)
       +            else:
       +                response['result'] = await f(*params)
       +        except BaseException as e:
       +            self.logger.exception("internal error while executing RPC")
       +            response['error'] = str(e)
       +        return web.json_response(response)
        
        
        class CommandsServer(AuthenticatedServer):
       t@@ -208,13 +220,12 @@ class CommandsServer(AuthenticatedServer):
                self.port = self.config.get('rpcport', 0)
                self.app = web.Application()
                self.app.router.add_post("/", self.handle)
       -        self.methods = jsonrpcserver.methods.Methods()
       -        self.methods.add(self.ping)
       -        self.methods.add(self.gui)
       +        self.register_method(self.ping)
       +        self.register_method(self.gui)
                self.cmd_runner = Commands(config=self.config, network=self.daemon.network, daemon=self.daemon)
                for cmdname in known_commands:
       -            self.methods.add(getattr(self.cmd_runner, cmdname))
       -        self.methods.add(self.run_cmdline)
       +            self.register_method(getattr(self.cmd_runner, cmdname))
       +        self.register_method(self.run_cmdline)
        
            async def run(self):
                self.runner = web.AppRunner(self.app)
       t@@ -276,9 +287,8 @@ class WatchTowerServer(AuthenticatedServer):
                self.lnwatcher = network.local_watchtower
                self.app = web.Application()
                self.app.router.add_post("/", self.handle)
       -        self.methods = jsonrpcserver.methods.Methods()
       -        self.methods.add(self.get_ctn)
       -        self.methods.add(self.add_sweep_tx)
       +        self.register_method(self.get_ctn)
       +        self.register_method(self.add_sweep_tx)
        
            async def run(self):
                self.runner = web.AppRunner(self.app)
   DIR diff --git a/electrum/lnworker.py b/electrum/lnworker.py
       t@@ -10,6 +10,7 @@ import time
        from typing import Optional, Sequence, Tuple, List, Dict, TYPE_CHECKING, NamedTuple, Union, Mapping
        import threading
        import socket
       +import aiohttp
        import json
        from datetime import datetime, timezone
        from functools import partial
       t@@ -25,7 +26,7 @@ from . import constants, util
        from . import keystore
        from .util import profiler
        from .invoices import PR_TYPE_LN, PR_UNPAID, PR_EXPIRED, PR_PAID, PR_INFLIGHT, PR_FAILED, PR_ROUTING, LNInvoice, LN_EXPIRY_NEVER
       -from .util import NetworkRetryManager
       +from .util import NetworkRetryManager, JsonRPCClient
        from .lnutil import LN_MAX_FUNDING_SAT
        from .keystore import BIP32_KeyStore
        from .bitcoin import COIN
       t@@ -525,12 +526,6 @@ class LNWallet(LNWorker):
            @ignore_exceptions
            @log_exceptions
            async def sync_with_remote_watchtower(self):
       -        import aiohttp
       -        from jsonrpcclient.clients.aiohttp_client import AiohttpClient
       -        class myAiohttpClient(AiohttpClient):
       -            async def request(self, *args, **kwargs):
       -                r = await super().request(*args, **kwargs)
       -                return r.data.result
                while True:
                    await asyncio.sleep(5)
                    watchtower_url = self.config.get('watchtower_url')
       t@@ -538,7 +533,9 @@ class LNWallet(LNWorker):
                        continue
                    try:
                        async with make_aiohttp_session(proxy=self.network.proxy) as session:
       -                    watchtower = myAiohttpClient(session, watchtower_url)
       +                    watchtower = JsonRPCClient(session, watchtower_url)
       +                    watchtower.add_method('get_ctn')
       +                    watchtower.add_method('add_sweep_tx')
                            for chan in self.channels.values():
                                await self.sync_channel_with_watchtower(chan, watchtower)
                    except aiohttp.client_exceptions.ClientConnectorError:
   DIR diff --git a/electrum/util.py b/electrum/util.py
       t@@ -1368,3 +1368,33 @@ class MySocksProxy(aiorpcx.SOCKSProxy):
                else:
                    raise NotImplementedError  # http proxy not available with aiorpcx
                return ret
       +
       +
       +class JsonRPCClient:
       +
       +    def __init__(self, session: aiohttp.ClientSession, url: str):
       +        self.session = session
       +        self.url = url
       +        self._id = 0
       +
       +    async def request(self, endpoint, *args):
       +        self._id += 1
       +        data = ('{"jsonrpc": "2.0", "id":"%d", "method": "%s", "params": %s }'
       +                % (self._id, endpoint, json.dumps(args)))
       +        async with self.session.post(self.url, data=data) as resp:
       +            if resp.status == 200:
       +                r = await resp.json()
       +                result = r.get('result')
       +                error = r.get('error')
       +                if error:
       +                    return 'Error: ' + str(error)
       +                else:
       +                    return result
       +            else:
       +                text = await resp.text()
       +                return 'Error: ' + str(text)
       +
       +    def add_method(self, endpoint):
       +        async def coro(*args):
       +            return await self.request(endpoint, *args)
       +        setattr(self, endpoint, coro)