Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Standardize revert exceptions between calls and transactions #527

Merged
merged 2 commits into from
May 18, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 58 additions & 17 deletions brownie/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
#!/usr/bin/python3

import json
import sys
from typing import Any, Type
from typing import Optional, Type

import psutil
import yaml

import brownie

# network

Expand Down Expand Up @@ -53,23 +55,62 @@ class MainnetUndefined(Exception):

class VirtualMachineError(Exception):

"""Raised when a call to a contract causes an EVM exception.

Attributes:
revert_msg: The returned error string, if any.
source: The contract source code where the revert occured, if available."""

def __init__(self, exc: Any) -> None:
if type(exc) is not dict:
"""
Raised when a call to a contract causes an EVM exception.

Attributes
----------
message : str
The full error message received from the RPC client.
revert_msg : str
The returned error string, if any.
revert_type : str
The error type.
pc : int
The program counter where the error was raised.
txid : str
The transaction ID that raised the error.
"""

def __init__(self, exc: ValueError) -> None:
try:
exc = yaml.safe_load(str(exc))
except Exception:
pass

if isinstance(exc, dict) and "message" in exc:
self.message: str = exc["message"]
try:
exc = eval(str(exc))
except SyntaxError:
exc = {"message": str(exc)}
self.revert_msg = msg = exc["message"]
self.source = exc.get("source", "")
txid, data = next((k, v) for k, v in exc["data"].items() if k.startswith("0x"))
except StopIteration:
return

self.txid: str = txid
self.revert_type: str = data["error"]
self.revert_msg: Optional[str] = data.get("reason")
self.pc: Optional[str] = data.get("program_counter")
self.source: str = ""
if self.revert_type == "revert":
self.pc -= 1
if self.revert_msg is None and self.revert_type in ("revert", "invalid opcode"):
self.revert_msg = brownie.project.build._get_dev_revert(self.pc)
else:
self.message = str(exc)

def __str__(self) -> str:
if not hasattr(self, "revert_type"):
return self.message
msg = self.revert_type
if self.revert_msg:
msg = f"{msg}: {self.revert_msg}"
if self.source:
msg = f"{msg}\n{self.source}"
super().__init__(msg)
return msg

def _with_attr(self, **kwargs) -> "VirtualMachineError":
for key, value in kwargs.items():
setattr(self, key, value)
return self


class EventLookupError(LookupError):
Expand Down Expand Up @@ -101,7 +142,7 @@ class ProjectNotFound(Exception):

class CompilerError(Exception):
def __init__(self, e: Type[psutil.Popen]) -> None:
err = [i["formattedMessage"] for i in json.loads(e.stdout_data)["errors"]]
err = [i["formattedMessage"] for i in yaml.safe_load(e.stdout_data)["errors"]]
super().__init__("Compiler returned the following errors:\n\n" + "\n".join(err))


Expand Down
38 changes: 15 additions & 23 deletions brownie/network/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,9 +276,14 @@ def deploy(
"data": HexBytes(data),
}
)
revert_data = None
exc, revert_data = None, None
except ValueError as e:
txid, revert_data = _raise_or_return_tx(e)
exc = VirtualMachineError(e)
if not hasattr(exc, "txid"):
raise exc from None
txid = exc.txid
revert_data = (exc.revert_msg, exc.pc, exc.revert_type)

receipt = TransactionReceipt(
txid, self, name=contract._name + ".constructor", revert_data=revert_data
)
Expand All @@ -299,7 +304,7 @@ def deploy(
undo_thread.start()

if receipt.status != 1:
receipt._raise_if_reverted()
receipt._raise_if_reverted(exc)
return receipt

add_thread.join()
Expand Down Expand Up @@ -368,9 +373,13 @@ def transfer(
tx["to"] = to_address(str(to))
try:
txid = self._transact(tx) # type: ignore
revert_data = None
exc, revert_data = None, None
except ValueError as e:
txid, revert_data = _raise_or_return_tx(e)
exc = VirtualMachineError(e)
if not hasattr(exc, "txid"):
raise exc from None
txid = exc.txid
revert_data = (exc.revert_msg, exc.pc, exc.revert_type)

receipt = TransactionReceipt(txid, self, silent=silent, revert_data=revert_data)
if rpc.is_active():
Expand All @@ -380,7 +389,7 @@ def transfer(
daemon=True,
)
undo_thread.start()
receipt._raise_if_reverted()
receipt._raise_if_reverted(exc)
return receipt


Expand Down Expand Up @@ -445,20 +454,3 @@ def _transact(self, tx: Dict) -> None:
self._check_for_revert(tx)
signed_tx = self._acct.sign_transaction(tx).rawTransaction # type: ignore
return web3.eth.sendRawTransaction(signed_tx)


def _raise_or_return_tx(exc: ValueError) -> Any:
try:
data = eval(str(exc))["data"]
txid = next(i for i in data.keys() if i[:2] == "0x")
reason = data[txid]["reason"] if "reason" in data[txid] else None
pc = data[txid]["program_counter"]
revert_type = data[txid]["error"]
if revert_type == "revert":
pc -= 1
return txid, [reason, pc, revert_type]
except SyntaxError:
raise exc
except Exception:
print("hrmmmmmm")
raise VirtualMachineError(exc) from None
16 changes: 5 additions & 11 deletions brownie/network/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

from brownie._config import CONFIG
from brownie.convert import EthAddress, Wei
from brownie.exceptions import RPCRequestError, VirtualMachineError
from brownie.exceptions import RPCRequestError
from brownie.project import build
from brownie.project.sources import highlight_source
from brownie.test import coverage
Expand Down Expand Up @@ -164,14 +164,8 @@ def __init__(
self.contract_name, self.fn_name = name.split(".", maxsplit=1)

# avoid querying the trace to get the revert string if possible
revert_msg, self._revert_pc, revert_type = revert_data or (None, None, None)
if revert_msg:
# revert message was returned
self._revert_msg = revert_msg
elif revert_type in ("revert", "invalid opcode"):
# check for dev revert string as a comment
self._revert_msg = build._get_dev_revert(self._revert_pc)
else:
self._revert_msg, self._revert_pc, revert_type = revert_data or (None, None, None)
if self._revert_msg is None and revert_type not in ("revert", "invalid_opcode"):
self._revert_msg = revert_type

# threaded to allow impatient users to ctrl-c to stop waiting in the console
Expand Down Expand Up @@ -265,7 +259,7 @@ def timestamp(self) -> Optional[int]:
return None
return web3.eth.getBlock(self.block_number)["timestamp"]

def _raise_if_reverted(self) -> None:
def _raise_if_reverted(self, exc: Any) -> None:
if self.status or CONFIG.mode == "console":
return
if self._revert_msg is None:
Expand All @@ -277,7 +271,7 @@ def _raise_if_reverted(self) -> None:
source = self._traceback_string()
else:
source = self._error_string(1)
raise VirtualMachineError({"message": self._revert_msg or "", "source": source})
raise exc._with_attr(source=source, revert_msg=self._revert_msg)

def _await_confirmation(self, silent: bool) -> None:
# await tx showing in mempool
Expand Down