From 5135957c96fc9f743ef9377556465ae3806044f1 Mon Sep 17 00:00:00 2001 From: Karl Bartel Date: Mon, 24 Feb 2020 17:22:22 +0100 Subject: [PATCH] Parse revert reason when `call` fails Solidity allows messages in `require` statements which can help a lot when debugging. This commit parses the revert reasons from failing `call`s and raises them as `SolidityError` exceptions. Closes https://github.com/ethereum/web3.py/issues/941. --- newsfragments/941.feature.rst | 1 + tests/core/manager/test_revert_reason.py | 39 ++++++++++++++++++++++++ web3/exceptions.py | 8 +++++ web3/manager.py | 32 +++++++++++++++++++ web3/types.py | 1 + 5 files changed, 81 insertions(+) create mode 100644 newsfragments/941.feature.rst create mode 100644 tests/core/manager/test_revert_reason.py diff --git a/newsfragments/941.feature.rst b/newsfragments/941.feature.rst new file mode 100644 index 0000000000..9c0f4764fe --- /dev/null +++ b/newsfragments/941.feature.rst @@ -0,0 +1 @@ +Raise `SolidityError` exceptions that contain the revert reason when a `call` fails. diff --git a/tests/core/manager/test_revert_reason.py b/tests/core/manager/test_revert_reason.py new file mode 100644 index 0000000000..1ded1687cb --- /dev/null +++ b/tests/core/manager/test_revert_reason.py @@ -0,0 +1,39 @@ +from web3.manager import ( + get_revert_reason, +) +from web3.types import ( + RPCResponse, +) + + +def test_get_revert_reason() -> None: + response = RPCResponse({ + 'jsonrpc': '2.0', + 'error': { + 'code': -32015, + 'message': 'VM execution error.', + 'data': ( + 'Reverted ' + '0x08c379a' + '00000000000000000000000000000000000000000000000000000000000000020' + '0000000000000000000000000000000000000000000000000000000000000016' + '6e6f7420616c6c6f77656420746f206d6f6e69746f7200000000000000000000' + ), + }, + 'id': 2987 + }) + assert get_revert_reason(response) == 'not allowed to monitor' + + +def test_get_revert_reason_without_reason() -> None: + """ In this case, no reason string is given in the solidity code """ + response = RPCResponse({ + 'jsonrpc': '2.0', + 'error': { + 'code': -32015, + 'message': 'VM execution error.', + 'data': 'Reverted 0x', + }, + 'id': 2987 + }) + assert get_revert_reason(response) is None diff --git a/web3/exceptions.py b/web3/exceptions.py index 2241d887e3..b363206fbe 100644 --- a/web3/exceptions.py +++ b/web3/exceptions.py @@ -169,3 +169,11 @@ class InvalidEventABI(ValueError): Raised when the event ABI is invalid. """ pass + + +class SolidityError(ValueError): + # Inherits from ValueError for backwards compatibility + """ + Raised on a solidity require/revert + """ + pass diff --git a/web3/manager.py b/web3/manager.py index 54b3e94a73..a2ca60efc2 100644 --- a/web3/manager.py +++ b/web3/manager.py @@ -28,6 +28,9 @@ from web3.datastructures import ( NamedElementOnion, ) +from web3.exceptions import ( + SolidityError, +) from web3.middleware import ( abi_middleware, attrdict_middleware, @@ -63,6 +66,32 @@ def apply_error_formatters( return response +def get_revert_reason(response: RPCResponse) -> str: + """ + Parse revert reason from response, return None if no revert happened. + + Reverts contain a `data` attribute with the following layout: + "Reverted " + Function selector for Error(string): 08c379a (4 bytes) + Data offset: 32 (32 bytes) + String length (32 bytes) + Reason strong (padded, use string length from above to get meaningful part) + """ + assert 'error' in response + if not isinstance(response['error'], dict): + return None + + data = response['error'].get('data', '') + # "Reverted", function selector and offset are always the same for revert errors + prefix = 'Reverted 0x08c379a00000000000000000000000000000000000000000000000000000000000000020' + if not data.startswith(prefix): + return None + + reason_length = int(data[len(prefix):len(prefix) + 64], 16) + reason = data[len(prefix) + 64:len(prefix) + 64 + reason_length * 2] + return bytes.fromhex(reason).decode('utf8') + + class RequestManager: logger = logging.getLogger("web3.RequestManager") @@ -150,6 +179,9 @@ def request_blocking( if "error" in response: apply_error_formatters(error_formatters, response) + revert_reason = get_revert_reason(response) + if revert_reason: + raise SolidityError(revert_reason) raise ValueError(response["error"]) return response['result'] diff --git a/web3/types.py b/web3/types.py index 66e042a38f..c218c01e83 100644 --- a/web3/types.py +++ b/web3/types.py @@ -109,6 +109,7 @@ class EventData(TypedDict): class RPCError(TypedDict): code: int message: str + data: Optional[str] class RPCResponse(TypedDict, total=False):