Repository: ethereum/web3.py

Issue:
Add optional user message to Web3Exception
### What was wrong?

It would be nice to add an optional user message on the `Web3Exception` class that would allow us to communicate info about the exception to users. It came up in the context of #2796. In that case, we only return the info from the node, but we get a lot of questions about what the node error info means. It would be nice if we could include the error message from the node along with a hint to the users.






Discussion:
reedsa: @pacrob Yeah I can add tests that throw this and assert on the `user_message`. Good call!

Patch:
diff --git a/newsfragments/3263.feature.rst b/newsfragments/3263.feature.rst
new file mode 100644
index 00000000..e9e0fe28
--- /dev/null
+++ b/newsfragments/3263.feature.rst
@@ -0,0 +1,1 @@
+Add ``user_message`` kwarg for human readable ``Web3Exception`` messages.
diff --git a/newsfragments/3289.bugfix.rst b/newsfragments/3289.bugfix.rst
new file mode 100644
index 00000000..939fa264
--- /dev/null
+++ b/newsfragments/3289.bugfix.rst
@@ -0,0 +1,1 @@
+Fix ``process_log()`` when parsing logs for events with indexed and non-indexed inputs. ``get_event_data()`` now compares log topics and event ABIs as hex values.
\ No newline at end of file
diff --git a/web3/_utils/events.py b/web3/_utils/events.py
index 543d33bf..ecab7620 100644
--- a/web3/_utils/events.py
+++ b/web3/_utils/events.py
@@ -68,6 +68,9 @@ from web3._utils.encoding import (
 from web3._utils.normalizers import (
     BASE_RETURN_NORMALIZERS,
 )
+from web3._utils.type_conversion import (
+    to_hex_if_bytes,
+)
 from web3.datastructures import (
     AttributeDict,
 )
@@ -228,8 +231,11 @@ def get_event_data(
         log_topics = log_entry["topics"]
     elif not log_entry["topics"]:
         raise MismatchedABI("Expected non-anonymous event to have 1 or more topics")
-    # type ignored b/c event_abi_to_log_topic(event_abi: Dict[str, Any])
-    elif event_abi_to_log_topic(event_abi) != log_entry["topics"][0]:  # type: ignore
+    elif (
+        # type ignored b/c event_abi_to_log_topic(event_abi: Dict[str, Any])
+        to_hex(event_abi_to_log_topic(event_abi))  # type: ignore
+        != to_hex_if_bytes(log_entry["topics"][0])
+    ):
         raise MismatchedABI("The event signature did not match the provided ABI")
     else:
         log_topics = log_entry["topics"][1:]
diff --git a/web3/exceptions.py b/web3/exceptions.py
index 217c4744..15429053 100644
--- a/web3/exceptions.py
+++ b/web3/exceptions.py
@@ -30,6 +30,16 @@ class Web3Exception(Exception):
             # deal with other exceptions
     """
 
+    def __init__(
+        self,
+        *args: Any,
+        user_message: Optional[str] = None,
+    ):
+        super().__init__(*args)
+
+        # Assign properties of Web3Exception
+        self.user_message = user_message
+
 
 class BadFunctionCallOutput(Web3Exception):
     """
@@ -274,6 +284,7 @@ class ContractLogicError(Web3Exception):
         message: Optional[str] = None,
         data: Optional[Union[str, Dict[str, str]]] = None,
     ):
+        super().__init__(message, data)
         self.message = message
         self.data = data
 
diff --git a/web3/manager.py b/web3/manager.py
index a94328f7..273b05f7 100644
--- a/web3/manager.py
+++ b/web3/manager.py
@@ -246,7 +246,11 @@ class RequestManager:
             if not isinstance(code, int):
                 _raise_bad_response_format(response, "error['code'] must be an integer")
             elif code == METHOD_NOT_FOUND:
-                raise MethodUnavailable(error)
+                raise MethodUnavailable(
+                    error,
+                    user_message="Check your node provider's API docs to see what "
+                    "methods are supported",
+                )
 
             # Errors must include a message
             if not isinstance(error.get("message"), str):


Test patch:
diff --git a/tests/core/core/block-utils/test_select_method_for_block_identifier.py b/tests/core/block-utils/test_select_method_for_block_identifier.py
similarity index 100%
rename from tests/core/core/block-utils/test_select_method_for_block_identifier.py
rename to tests/core/block-utils/test_select_method_for_block_identifier.py
diff --git a/tests/core/contracts/test_extracting_event_data.py b/tests/core/contracts/test_extracting_event_data.py
index 630a2c04..07fdcd22 100644
--- a/tests/core/contracts/test_extracting_event_data.py
+++ b/tests/core/contracts/test_extracting_event_data.py
@@ -143,6 +143,115 @@ def test_event_data_extraction(
     assert event_data["event"] == event_name
 
 
+def test_event_data_with_ordered_indexed_inputs(w3):
+    event_abi = {
+        "anonymous": False,
+        "inputs": [
+            {
+                "indexed": True,
+                "internalType": "uint64",
+                "name": "nonce",
+                "type": "uint64",
+            },
+            {
+                "indexed": True,
+                "internalType": "address",
+                "name": "burnToken",
+                "type": "address",
+            },
+            {
+                "indexed": False,
+                "internalType": "uint256",
+                "name": "amount",
+                "type": "uint256",
+            },
+            {
+                "indexed": True,
+                "internalType": "address",
+                "name": "depositor",
+                "type": "address",
+            },
+            {
+                "indexed": False,
+                "internalType": "bytes32",
+                "name": "mintRecipient",
+                "type": "bytes32",
+            },
+            {
+                "indexed": False,
+                "internalType": "uint32",
+                "name": "destinationDomain",
+                "type": "uint32",
+            },
+            {
+                "indexed": False,
+                "internalType": "bytes32",
+                "name": "destinationTokenMessenger",
+                "type": "bytes32",
+            },
+            {
+                "indexed": False,
+                "internalType": "bytes32",
+                "name": "destinationCaller",
+                "type": "bytes32",
+            },
+        ],
+        "name": "DepositForBurn",
+        "type": "event",
+    }
+    log_entry = {
+        "name": "DepositForBurn",
+        "topics": (
+            "0x2fa9ca894982930190727e75500a97d8dc500233a5065e0f3126c48fbe0343c0",
+            w3.to_bytes(
+                hexstr="0x0000000000000000000000000000000000000000000000000000000000014f45"  # noqa: E501
+            ),
+            w3.to_bytes(
+                hexstr="0x" + "af88d065e77c8cC2239327C5EDb3A432268e5831".zfill(64)
+            ),
+            w3.to_bytes(
+                hexstr="0x" + "02Ae4716B9D5d48Db1445814b0eDE39f5c28264B".zfill(64)
+            ),
+        ),
+        "data": w3.to_bytes(
+            hexstr="0x00000000000000000000000000000000000000000000000000000002962f766700000000000000000000000065f2145693be3e75b8cfb2e318a3a74d057e6c7b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000bd3fa81b58ba92a82136038b25adec7066af31550000000000000000000000000000000000000000000000000000000000000000"  # noqa: E501
+        ),
+        "logIndex": 1,
+        "transactionIndex": 1,
+        "transactionHash": "1234",
+        "address": "0x19330d10d9cc8751218eaf51e8885d058642e08a",
+        "blockHash": "",
+        "blockNumber": "",
+    }
+    event_data = get_event_data(w3.codec, event_abi, log_entry)
+    expected = {
+        "args": {
+            "nonce": 85829,
+            "burnToken": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
+            "depositor": "0x02Ae4716B9D5d48Db1445814b0eDE39f5c28264B",
+            "amount": 11109627495,
+            "mintRecipient": b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00e\xf2\x14V\x93\xbe>u\xb8\xcf\xb2\xe3\x18\xa3\xa7M\x05~l{",  # noqa: E501
+            "destinationDomain": 0,
+            "destinationTokenMessenger": b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xbd?\xa8\x1bX\xba\x92\xa8!6\x03\x8b%\xad\xecpf\xaf1U",  # noqa: E501
+            "destinationCaller": b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",  # noqa: E501
+        },
+        "event": "DepositForBurn",
+        "logIndex": 1,
+        "transactionIndex": 1,
+        "transactionHash": "1234",
+        "address": "0x19330d10d9cc8751218eaf51e8885d058642e08a",
+        "blockHash": "",
+        "blockNumber": "",
+    }
+
+    assert event_data["args"] == expected["args"]
+    assert event_data["blockHash"] == expected["blockHash"]
+    assert event_data["blockNumber"] == expected["blockNumber"]
+    assert event_data["transactionIndex"] == expected["transactionIndex"]
+    assert is_same_address(event_data["address"], expected["address"])
+    assert event_data["event"] == expected["event"]
+
+
 @pytest.mark.parametrize(
     "call_args,expected_args",
     (
diff --git a/tests/core/exceptions/test_exceptions.py b/tests/core/exceptions/test_exceptions.py
new file mode 100644
index 00000000..e613cb51
--- /dev/null
+++ b/tests/core/exceptions/test_exceptions.py
@@ -0,0 +1,32 @@
+import pytest
+
+from web3.exceptions import (
+    Web3Exception,
+)
+
+
+def test_web3exception_with_user_message():
+    with pytest.raises(Web3Exception) as exception:
+        raise Web3Exception(user_message="This failed!")
+    assert exception.type is Web3Exception
+    assert exception.value.user_message == "This failed!"
+
+
+def test_web3exception_with_kwargs():
+    with pytest.raises(TypeError) as exception:
+        raise Web3Exception(data={"message": "Unable to fulfill your request."})
+
+    # For Python > 3.9, str exception includes 'Web3Exception.'
+    expected = "__init__() got an unexpected keyword argument 'data'"
+    actual = str(exception.value)
+    assert exception.type is TypeError
+    assert hasattr(exception.value, "data") is False
+    assert expected in actual
+
+
+def test_web3exception_with_args():
+    with pytest.raises(Web3Exception) as exception:
+        raise Web3Exception("failed")
+    assert exception.type is Web3Exception
+    assert exception.value.user_message is None
+    assert exception.value.args[0] == "failed"