Skip to content

Commit

Permalink
Parse revert reason when call fails
Browse files Browse the repository at this point in the history
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 ethereum#941.
  • Loading branch information
karlb committed Feb 24, 2020
1 parent fc997a3 commit a41c527
Show file tree
Hide file tree
Showing 4 changed files with 74 additions and 0 deletions.
35 changes: 35 additions & 0 deletions tests/core/manager/test_revert_reason.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
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
8 changes: 8 additions & 0 deletions web3/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
30 changes: 30 additions & 0 deletions web3/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@
from web3.datastructures import (
NamedElementOnion,
)
from web3.exceptions import (
SolidityError,
)
from web3.middleware import (
abi_middleware,
attrdict_middleware,
Expand Down Expand Up @@ -63,6 +66,30 @@ 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', '')
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]
return reason


class RequestManager:
logger = logging.getLogger("web3.RequestManager")

Expand Down Expand Up @@ -150,6 +177,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']
Expand Down
1 change: 1 addition & 0 deletions web3/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ class EventData(TypedDict):
class RPCError(TypedDict):
code: int
message: str
data: Optional[str]


class RPCResponse(TypedDict, total=False):
Expand Down

0 comments on commit a41c527

Please sign in to comment.