URI: 
       tReimplement JSON-RPC errors as a class. - obelisk - Electrum server using libbitcoin as its backend
  HTML git clone https://git.parazyd.org/obelisk
   DIR Log
   DIR Files
   DIR Refs
   DIR README
   DIR LICENSE
       ---
   DIR commit e8ea79bf3393e0a427ece4add23a41728206191a
   DIR parent 720e15a7f78eecba1720909c66bcf28a86975672
  HTML Author: parazyd <parazyd@dyne.org>
       Date:   Thu, 15 Apr 2021 11:59:30 +0200
       
       Reimplement JSON-RPC errors as a class.
       
       Diffstat:
         D obelisk/errors.py                   |      58 ------------------------------
         A obelisk/errors_jsonrpc.py           |      52 +++++++++++++++++++++++++++++++
         R obelisk/libbitcoin_errors.py -> ob… |       0 
         M obelisk/protocol.py                 |     103 +++++++++++++++++--------------
         M obelisk/zeromq.py                   |       5 +++--
       
       5 files changed, 112 insertions(+), 106 deletions(-)
       ---
   DIR diff --git a/obelisk/errors.py b/obelisk/errors.py
       t@@ -1,58 +0,0 @@
       -#!/usr/bin/env python3
       -# Copyright (C) 2020-2021 Ivan J. <parazyd@dyne.org>
       -#
       -# This file is part of obelisk
       -#
       -# This program is free software: you can redistribute it and/or modify
       -# it under the terms of the GNU Affero General Public License version 3
       -# as published by the Free Software Foundation.
       -#
       -# This program is distributed in the hope that it will be useful,
       -# but WITHOUT ANY WARRANTY; without even the implied warranty of
       -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
       -# GNU Affero General Public License for more details.
       -#
       -# You should have received a copy of the GNU Affero General Public License
       -# along with this program.  If not, see <http://www.gnu.org/licenses/>.
       -"""JSON-RPC errors
       -https://www.jsonrpc.org/specification#error_object
       -"""
       -
       -ERRORS = {
       -    "invalidparams": {
       -        "error": {
       -            "code": -32602,
       -            "message": "invalid parameters"
       -        }
       -    },
       -    "internalerror": {
       -        "error": {
       -            "code": -32603,
       -            "message": "internal error"
       -        }
       -    },
       -    "parseerror": {
       -        "error": {
       -            "code": -32700,
       -            "message": "parse error"
       -        }
       -    },
       -    "invalidrequest": {
       -        "error": {
       -            "code": -32600,
       -            "message": "invalid request"
       -        }
       -    },
       -    "nomethod": {
       -        "error": {
       -            "code": -32601,
       -            "message": "method not found"
       -        }
       -    },
       -    "protonotsupported": {
       -        "error": {
       -            "code": -32100,
       -            "message": "client protocol version is not supported",
       -        }
       -    },
       -}
   DIR diff --git a/obelisk/errors_jsonrpc.py b/obelisk/errors_jsonrpc.py
       t@@ -0,0 +1,52 @@
       +#!/usr/bin/env python3
       +# Copyright (C) 2021 Ivan J. <parazyd@dyne.org>
       +#
       +# This file is part of obelisk
       +#
       +# This program is free software: you can redistribute it and/or modify
       +# it under the terms of the GNU Affero General Public License version 3
       +# as published by the Free Software Foundation.
       +#
       +# This program is distributed in the hope that it will be useful,
       +# but WITHOUT ANY WARRANTY; without even the implied warranty of
       +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
       +# GNU Affero General Public License for more details.
       +#
       +# You should have received a copy of the GNU Affero General Public License
       +# along with this program.  If not, see <http://www.gnu.org/licenses/>.
       +"""JSON-RPC errors: https://www.jsonrpc.org/specification#error_object"""
       +
       +
       +class JsonRPCError:
       +
       +    def __init__(self):
       +        return
       +
       +    @staticmethod
       +    def invalidrequest():
       +        return {"error": {"code": -32600, "message": "invalid request"}}
       +
       +    @staticmethod
       +    def methodnotfound():
       +        return {"error": {"code": -32601, "message": "method not found"}}
       +
       +    @staticmethod
       +    def invalidparams():
       +        return {"error": {"code": -32602, "message": "invalid parameters"}}
       +
       +    @staticmethod
       +    def internalerror():
       +        return {"error": {"code": -32603, "message": "internal error"}}
       +
       +    @staticmethod
       +    def parseerror():
       +        return {"error": {"code": -37200, "message": "parse error"}}
       +
       +    @staticmethod
       +    def protonotsupported():
       +        return {
       +            "error": {
       +                "code": -32100,
       +                "message": "protocol version unsupported"
       +            }
       +        }
   DIR diff --git a/obelisk/libbitcoin_errors.py b/obelisk/errors_libbitcoin.py
   DIR diff --git a/obelisk/protocol.py b/obelisk/protocol.py
       t@@ -21,7 +21,7 @@ import asyncio
        import json
        from binascii import unhexlify
        
       -from obelisk.errors import ERRORS
       +from obelisk.errors_jsonrpc import JsonRPCError
        from obelisk.merkle import merkle_branch, merkle_branch_and_root
        from obelisk.util import (
            bh2u,
       t@@ -213,7 +213,8 @@ class ElectrumProtocol(asyncio.Protocol):  # pylint: disable=R0904,R0902
                func = self.methodmap.get(method)
                if not func:
                    self.log.error("Unhandled method %s, query=%s", method, query)
       -            return await self._send_reply(writer, ERRORS["nomethod"], query)
       +            return await self._send_reply(writer, JsonRPCError.methodnotfound(),
       +                                          query)
                resp = await func(writer, query)
                return await self._send_reply(writer, resp, query)
        
       t@@ -222,22 +223,22 @@ class ElectrumProtocol(asyncio.Protocol):  # pylint: disable=R0904,R0902
                Return the block header at the given height.
                """
                if "params" not in query or len(query["params"]) < 1:
       -            return ERRORS["invalidparams"]
       +            return JsonRPCError.invalidparams()
                index = query["params"][0]
                cp_height = query["params"][1] if len(query["params"]) == 2 else 0
        
                if not is_non_negative_integer(index):
       -            return ERRORS["invalidparams"]
       +            return JsonRPCError.invalidparams()
                if not is_non_negative_integer(cp_height):
       -            return ERRORS["invalidparams"]
       +            return JsonRPCError.invalidparams()
                if cp_height != 0 and not index <= cp_height:
       -            return ERRORS["invalidparams"]
       +            return JsonRPCError.invalidparams()
        
                if cp_height == 0:
                    _ec, header = await self.bx.fetch_block_header(index)
                    if _ec and _ec != 0:
                        self.log.debug("Got error: %s", repr(_ec))
       -                return ERRORS["internalerror"]
       +                return JsonRPCError.internalerror()
                    return {"result": safe_hexlify(header)}
        
                cp_headers = []
       t@@ -246,7 +247,7 @@ class ElectrumProtocol(asyncio.Protocol):  # pylint: disable=R0904,R0902
                    _ec, data = await self.bx.fetch_block_header(i)
                    if _ec and _ec != 0:
                        self.log.debug("Got error: %s", repr(_ec))
       -                return ERRORS["internalerror"]
       +                return JsonRPCError.internalerror()
                    cp_headers.append(data)
        
                # TODO: Review
       t@@ -264,7 +265,7 @@ class ElectrumProtocol(asyncio.Protocol):  # pylint: disable=R0904,R0902
                Return a concatenated chunk of block headers from the main chain.
                """
                if "params" not in query or len(query["params"]) < 2:
       -            return ERRORS["invalidparams"]
       +            return JsonRPCError.invalidparams()
                # Electrum doesn't allow max_chunk_size to be less than 2016
                # gopher://bitreich.org/9/memecache/convenience-store.mkv
                # TODO: cp_height
       t@@ -273,9 +274,9 @@ class ElectrumProtocol(asyncio.Protocol):  # pylint: disable=R0904,R0902
                count = query["params"][1]
        
                if not is_non_negative_integer(start_height):
       -            return ERRORS["invalidparams"]
       +            return JsonRPCError.invalidparams()
                if not is_non_negative_integer(count):
       -            return ERRORS["invalidparams"]
       +            return JsonRPCError.invalidparams()
        
                count = min(count, max_chunk_size)
                headers = bytearray()
       t@@ -283,7 +284,7 @@ class ElectrumProtocol(asyncio.Protocol):  # pylint: disable=R0904,R0902
                    _ec, data = await self.bx.fetch_block_header(i)
                    if _ec and _ec != 0:
                        self.log.debug("Got error: %s", repr(_ec))
       -                return ERRORS["internalerror"]
       +                return JsonRPCError.internalerror()
                    headers.extend(data)
        
                resp = {
       t@@ -325,11 +326,11 @@ class ElectrumProtocol(asyncio.Protocol):  # pylint: disable=R0904,R0902
                _ec, height = await self.bx.fetch_last_height()
                if _ec and _ec != 0:
                    self.log.debug("Got error: %s", repr(_ec))
       -            return ERRORS["internalerror"]
       +            return JsonRPCError.internalerror()
                _ec, tip_header = await self.bx.fetch_block_header(height)
                if _ec and _ec != 0:
                    self.log.debug("Got error: %s", repr(_ec))
       -            return ERRORS["internalerror"]
       +            return JsonRPCError.internalerror()
        
                self.tasks.append(asyncio.create_task(self.header_notifier(writer)))
                ret = {"height": height, "hex": safe_hexlify(tip_header)}
       t@@ -348,15 +349,15 @@ class ElectrumProtocol(asyncio.Protocol):  # pylint: disable=R0904,R0902
                Return the confirmed and unconfirmed balances of a script hash.
                """
                if "params" not in query or len(query["params"]) != 1:
       -            return ERRORS["invalidparams"]
       +            return JsonRPCError.invalidparams()
        
                if not is_hash256_str(query["params"][0]):
       -            return ERRORS["invalidparams"]
       +            return JsonRPCError.invalidparams()
        
                _ec, data = await self.bx.fetch_balance(query["params"][0])
                if _ec and _ec != 0:
                    self.log.debug("Got error: %s", repr(_ec))
       -            return ERRORS["internalerror"]
       +            return JsonRPCError.internalerror()
        
                # TODO: confirmed/unconfirmed, see what's happening in libbitcoin
                ret = {"confirmed": data, "unconfirmed": 0}
       t@@ -367,15 +368,15 @@ class ElectrumProtocol(asyncio.Protocol):  # pylint: disable=R0904,R0902
                Return the confirmed and unconfirmed history of a script hash.
                """
                if "params" not in query or len(query["params"]) != 1:
       -            return ERRORS["invalidparams"]
       +            return JsonRPCError.invalidparams()
        
                if not is_hash256_str(query["params"][0]):
       -            return ERRORS["invalidparams"]
       +            return JsonRPCError.invalidparams()
        
                _ec, data = await self.bx.fetch_history4(query["params"][0])
                if _ec and _ec != 0:
                    self.log.debug("Got error: %s", repr(_ec))
       -            return ERRORS["internalerror"]
       +            return JsonRPCError.internalerror()
        
                self.log.debug("hist: %s", data)
                ret = []
       t@@ -398,6 +399,7 @@ class ElectrumProtocol(asyncio.Protocol):  # pylint: disable=R0904,R0902
                """Method: blockchain.scripthash.get_mempool
                Return the unconfirmed transactions of a script hash.
                """
       +        # TODO: Implement
                return
        
            async def blockchain_scripthash_listunspent(self, writer, query):  # pylint: disable=W0613
       t@@ -405,16 +407,16 @@ class ElectrumProtocol(asyncio.Protocol):  # pylint: disable=R0904,R0902
                Return an ordered list of UTXOs sent to a script hash.
                """
                if "params" not in query or len(query["params"]) != 1:
       -            return ERRORS["invalidparams"]
       +            return JsonRPCError.invalidparams()
        
                scripthash = query["params"][0]
                if not is_hash256_str(scripthash):
       -            return ERRORS["invalidparams"]
       +            return JsonRPCError.invalidparams()
        
                _ec, utxo = await self.bx.fetch_utxo(scripthash)
                if _ec and _ec != 0:
                    self.log.debug("Got error: %s", repr(_ec))
       -            return ERRORS["internalerror"]
       +            return JsonRPCError.internalerror()
        
                # TODO: Check mempool
                ret = []
       t@@ -445,15 +447,15 @@ class ElectrumProtocol(asyncio.Protocol):  # pylint: disable=R0904,R0902
                Subscribe to a script hash.
                """
                if "params" not in query or len(query["params"]) != 1:
       -            return ERRORS["invalidparamas"]
       +            return JsonRPCError.invalidparams()
        
                scripthash = query["params"][0]
                if not is_hash256_str(scripthash):
       -            return ERRORS["invalidparams"]
       +            return JsonRPCError.invalidparams()
        
                _ec, history = await self.bx.fetch_history4(scripthash)
                if _ec and _ec != 0:
       -            return ERRORS["internalerror"]
       +            return JsonRPCError.internalerror()
        
                task = asyncio.create_task(self.scripthash_notifier(writer, scripthash))
                self.sh_subscriptions[scripthash] = {"task": task}
       t@@ -491,11 +493,11 @@ class ElectrumProtocol(asyncio.Protocol):  # pylint: disable=R0904,R0902
                if its status changes.
                """
                if "params" not in query or len(query["params"]) != 1:
       -            return ERRORS["invalidparams"]
       +            return JsonRPCError.invalidparams()
        
                scripthash = query["params"][0]
                if not is_hash256_str(scripthash):
       -            return ERRORS["invalidparams"]
       +            return JsonRPCError.invalidparams()
        
                if scripthash in self.sh_subscriptions:
                    self.sh_subscriptions[scripthash]["task"].cancel()
       t@@ -511,16 +513,16 @@ class ElectrumProtocol(asyncio.Protocol):  # pylint: disable=R0904,R0902
                """
                # Note: Not yet implemented in bs v4
                if "params" not in query or len(query["params"]) != 1:
       -            return ERRORS["invalidparams"]
       +            return JsonRPCError.invalidparams()
        
                hextx = query["params"][0]
                if not is_hex_str(hextx):
       -            return ERRORS["invalidparams"]
       +            return JsonRPCError.invalidparams()
        
                _ec, _ = await self.bx.broadcast_transaction(unhexlify(hextx)[::-1])
                if _ec and _ec != 0:
                    self.log.debug("Got error: %s", repr(_ec))
       -            return ERRORS["internalerror"]
       +            return JsonRPCError.internalerror()
        
                rawtx = unhexlify(hextx)
                txid = double_sha256(rawtx)
       t@@ -531,7 +533,8 @@ class ElectrumProtocol(asyncio.Protocol):  # pylint: disable=R0904,R0902
                Return a raw transaction.
                """
                if "params" not in query or len(query["params"]) < 1:
       -            return ERRORS["invalidparams"]
       +            return JsonRPCError.invalidparams()
       +
                tx_hash = query["params"][0]
                verbose = query["params"][1] if len(query["params"]) > 1 else False
        
       t@@ -539,7 +542,7 @@ class ElectrumProtocol(asyncio.Protocol):  # pylint: disable=R0904,R0902
                _ec, rawtx = await self.bx.fetch_mempool_transaction(tx_hash)
                if _ec and _ec != 0:
                    self.log.debug("Got error: %s", repr(_ec))
       -            return ERRORS["internalerror"]
       +            return JsonRPCError.internalerror()
        
                # Behaviour is undefined in spec
                if not rawtx:
       t@@ -547,7 +550,7 @@ class ElectrumProtocol(asyncio.Protocol):  # pylint: disable=R0904,R0902
        
                if verbose:
                    # TODO: Help needed
       -            return ERRORS["invalidrequest"]
       +            return JsonRPCError.invalidrequest()
        
                return {"result": bh2u(rawtx)}
        
       t@@ -557,19 +560,20 @@ class ElectrumProtocol(asyncio.Protocol):  # pylint: disable=R0904,R0902
                hash and height.
                """
                if "params" not in query or len(query["params"]) != 2:
       -            return ERRORS["invalidparams"]
       +            return JsonRPCError.invalidparams()
       +
                tx_hash = query["params"][0]
                height = query["params"][1]
        
                if not is_hash256_str(tx_hash):
       -            return ERRORS["invalidparams"]
       +            return JsonRPCError.invalidparams()
                if not is_non_negative_integer(height):
       -            return ERRORS["invalidparams"]
       +            return JsonRPCError.invalidparams()
        
                _ec, hashes = await self.bx.fetch_block_transaction_hashes(height)
                if _ec and _ec != 0:
                    self.log.debug("Got error: %s", repr(_ec))
       -            return ERRORS["internalerror"]
       +            return JsonRPCError.internalerror()
        
                # Decouple from tuples
                hashes = [i[0] for i in hashes]
       t@@ -589,25 +593,26 @@ class ElectrumProtocol(asyncio.Protocol):  # pylint: disable=R0904,R0902
                block height and a position in the block.
                """
                if "params" not in query or len(query["params"]) < 2:
       -            return ERRORS["invalidparams"]
       +            return JsonRPCError.invalidparams()
       +
                height = query["params"][0]
                tx_pos = query["params"][1]
                merkle = query["params"][2] if len(query["params"]) > 2 else False
        
                if not is_non_negative_integer(height):
       -            return ERRORS["invalidparams"]
       +            return JsonRPCError.invalidparams()
                if not is_non_negative_integer(tx_pos):
       -            return ERRORS["invalidparams"]
       +            return JsonRPCError.invalidparams()
                if not is_boolean(merkle):
       -            return ERRORS["invalidparams"]
       +            return JsonRPCError.invalidparams()
        
                _ec, hashes = await self.bx.fetch_block_transaction_hashes(height)
                if _ec and _ec != 0:
                    self.log.debug("Got error: %s", repr(_ec))
       -            return ERRORS["internalerror"]
       +            return JsonRPCError.internalerror()
        
                if len(hashes) - 1 < tx_pos:
       -            return ERRORS["internalerror"]
       +            return JsonRPCError.internalerror()
        
                # Decouple from tuples
                hashes = [i[0] for i in hashes]
       t@@ -615,6 +620,7 @@ class ElectrumProtocol(asyncio.Protocol):  # pylint: disable=R0904,R0902
        
                if not merkle:
                    return {"result": txid}
       +
                branch = merkle_branch(hashes, tx_pos)
                return {"result": {"tx_hash": txid, "merkle": branch}}
        
       t@@ -689,13 +695,18 @@ class ElectrumProtocol(asyncio.Protocol):  # pylint: disable=R0904,R0902
                Identify the client to the server and negotiate the protocol version.
                """
                if "params" not in query or len(query["params"]) != 2:
       -            return ERRORS["invalidparams"]
       +            return JsonRPCError.invalidparams()
       +
                client_ver = query["params"][1]
       +
                if isinstance(client_ver, list):
                    client_min, client_max = client_ver[0], client_ver[1]
                else:
                    client_min = client_max = client_ver
       +
                version = min(client_max, SERVER_PROTO_MAX)
       +
                if version < max(client_min, SERVER_PROTO_MIN):
       -            return ERRORS["protonotsupported"]
       +            return JsonRPCError.protonotsupported()
       +
                return {"result": [f"obelisk {VERSION}", version]}
   DIR diff --git a/obelisk/zeromq.py b/obelisk/zeromq.py
       t@@ -24,7 +24,8 @@ from random import randint
        import zmq
        import zmq.asyncio
        
       -from obelisk.libbitcoin_errors import make_error_code, ErrorCode
       +from obelisk.errors_libbitcoin import make_error_code, ErrorCode
       +from obelisk.util import hash_to_hex_str
        
        
        def create_random_id():
       t@@ -372,7 +373,7 @@ class Client:
                        },
                        height,
                        value,
       -                checksum(tx_hash[::-1].hex(), index),
       +                checksum(hash_to_hex_str(tx_hash), index),
                    )
        
                rows = unpack_table("<BI32sIQ", raw_points)