core-lightning/contrib/pyln-testing/pyln/testing/btcproxy.py
Rusty Russell 7c3aa9477f pyproject.toml: Update ancient flake8
v4.0 no longer works for me (see below, and widely reported elsewhere).
v5.0 doesn't understand f strings, and creates a flood of complaints.
v6.0 requires python >= 3.8.1, so we need to update that.
v7.0 is the latest, but why push it.

```
make check-python-flake8
Traceback (most recent call last):
  File "/home/rusty/.cache/pypoetry/virtualenvs/cln-meta-project-BgKQHyxC-py3.12/bin/flake8", line 8, in <module>
    sys.exit(main())
             ^^^^^^
  File "/home/rusty/.cache/pypoetry/virtualenvs/cln-meta-project-BgKQHyxC-py3.12/lib/python3.12/site-packages/flake8/main/cli.py", line 22, in main
    app.run(argv)
  File "/home/rusty/.cache/pypoetry/virtualenvs/cln-meta-project-BgKQHyxC-py3.12/lib/python3.12/site-packages/flake8/main/application.py", line 375, in run
    self._run(argv)
  File "/home/rusty/.cache/pypoetry/virtualenvs/cln-meta-project-BgKQHyxC-py3.12/lib/python3.12/site-packages/flake8/main/application.py", line 363, in _run
    self.initialize(argv)
  File "/home/rusty/.cache/pypoetry/virtualenvs/cln-meta-project-BgKQHyxC-py3.12/lib/python3.12/site-packages/flake8/main/application.py", line 343, in initialize
    self.find_plugins(config_finder)
  File "/home/rusty/.cache/pypoetry/virtualenvs/cln-meta-project-BgKQHyxC-py3.12/lib/python3.12/site-packages/flake8/main/application.py", line 157, in find_plugins
    self.check_plugins = plugin_manager.Checkers(local_plugins.extension)
                         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/rusty/.cache/pypoetry/virtualenvs/cln-meta-project-BgKQHyxC-py3.12/lib/python3.12/site-packages/flake8/plugins/manager.py", line 363, in __init__
    self.manager = PluginManager(
                   ^^^^^^^^^^^^^^
  File "/home/rusty/.cache/pypoetry/virtualenvs/cln-meta-project-BgKQHyxC-py3.12/lib/python3.12/site-packages/flake8/plugins/manager.py", line 243, in __init__
    self._load_entrypoint_plugins()
  File "/home/rusty/.cache/pypoetry/virtualenvs/cln-meta-project-BgKQHyxC-py3.12/lib/python3.12/site-packages/flake8/plugins/manager.py", line 261, in _load_entrypoint_plugins
    eps = importlib_metadata.entry_points().get(self.namespace, ())
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'EntryPoints' object has no attribute 'get'
make: *** [Makefile:535: check-python-flake8] Error 1
```

Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
2024-06-19 12:34:41 +02:00

111 lines
3.9 KiB
Python

""" A bitcoind proxy that allows instrumentation and canned responses
"""
from flask import Flask, request # type: ignore
from bitcoin.rpc import JSONRPCError # type: ignore
from bitcoin.rpc import RawProxy as BitcoinProxy # type: ignore
from cheroot.wsgi import Server # type: ignore
from cheroot.wsgi import PathInfoDispatcher # type: ignore
import decimal
import flask # type: ignore
import json
import logging
import threading
class DecimalEncoder(json.JSONEncoder):
"""By default json.dumps does not handle Decimals correctly, so we override it's handling
"""
def default(self, o):
if isinstance(o, decimal.Decimal):
return "{:.8f}".format(float(o))
return super(DecimalEncoder, self).default(o)
class BitcoinRpcProxy(object):
def __init__(self, bitcoind, rpcport=0):
self.app = Flask("BitcoindProxy")
self.app.add_url_rule("/", "API entrypoint", self.proxy, methods=['POST'])
self.rpcport = rpcport
self.mocks = {}
self.mock_counts = {}
self.bitcoind = bitcoind
self.request_count = 0
def _handle_request(self, r):
brpc = BitcoinProxy(btc_conf_file=self.bitcoind.conf_file)
method = r['method']
# If we have set a mock for this method reply with that instead of
# forwarding the request.
if method in self.mocks and isinstance(self.mocks[method], dict):
ret = {}
ret['id'] = r['id']
ret['error'] = None
ret['result'] = self.mocks[method]
self.mock_counts[method] += 1
return ret
elif method in self.mocks and callable(self.mocks[method]):
self.mock_counts[method] += 1
return self.mocks[method](r)
try:
reply = {
"result": brpc._call(r['method'], *r['params']),
"error": None,
"id": r['id']
}
except JSONRPCError as e:
reply = {
"error": e.error,
"code": -32603,
"id": r['id']
}
self.request_count += 1
return reply
def proxy(self):
r = json.loads(request.data.decode('ASCII'))
if isinstance(r, list):
reply = [self._handle_request(subreq) for subreq in r]
else:
reply = self._handle_request(r)
response = flask.Response(json.dumps(reply, cls=DecimalEncoder))
response.headers['Content-Type'] = 'application/json'
return response
def start(self):
d = PathInfoDispatcher({'/': self.app})
self.server = Server(('0.0.0.0', self.rpcport), d)
self.proxy_thread = threading.Thread(target=self.server.start)
self.proxy_thread.daemon = True
self.proxy_thread.start()
# Now that bitcoind is running on the real rpcport, let's tell all
# future callers to talk to the proxyport. We use the bind_addr as a
# signal that the port is bound and accepting connections.
while self.server.bind_addr[1] == 0:
pass
self.rpcport = self.server.bind_addr[1]
logging.debug("BitcoinRpcProxy proxying incoming port {} to {}".format(self.rpcport, self.bitcoind.rpcport))
def stop(self):
self.server.stop()
self.proxy_thread.join()
logging.debug("BitcoinRpcProxy shut down after processing {} requests".format(self.request_count))
def mock_rpc(self, method, response=None):
"""Mock the response to a future RPC call of @method
The response can either be a dict with the full JSON-RPC response, or a
function that returns such a response. If the response is None the mock
is removed and future calls will be passed through to bitcoind again.
"""
if response is not None:
self.mocks[method] = response
self.mock_counts[method] = 0
elif method in self.mocks:
del self.mocks[method]