Skip to content

Commit

Permalink
Merge branch 'master' of github.com:vyperlang/titanoboa into zksync
Browse files Browse the repository at this point in the history
  • Loading branch information
DanielSchiavini committed May 21, 2024
2 parents 79ff1b7 + b6904a9 commit 517e7df
Show file tree
Hide file tree
Showing 8 changed files with 105 additions and 136 deletions.
41 changes: 40 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,46 @@ Cast current deployed addresses to vyper contract

### Jupyter Integration

You can use Jupyter to execute titanoboa code in network mode from your browser using any wallet, using `boa.integrations.jupyter.BrowserSigner` as a drop-in replacement for `eth_account.Account`. For a full example, please see [this example Jupyter notebook](examples/jupyter_browser_signer.ipynb)
You can use Jupyter to execute titanoboa code in network mode from your browser using any wallet.
We provide a `BrowserSigner` as a drop-in replacement for `eth_account.Account`.
The `BrowserRPC` may be used to interact with the RPC server from the browser.

For a full example, please see [this example Jupyter notebook](examples/jupyter_browser_signer.ipynb)

#### JupyterLab

Before being able to use the plugin, you need to install it.
You can do this by running the following command in the terminal:

```bash
pip install titanoboa
jupyter lab extension enable boa
```
To activate our IPython extension, you need to run the following command in the notebook:
```jupyter
%load_ext boa.ipython
```

For ease of use, add the following to `ipython_config.py`:
```python
c.InteractiveShellApp.extensions = ["boa.ipython"]
c.InteractiveShellApp.exec_lines = ['import boa']
```

We provide a multi-user setup with JupyterLab in [try.vyperlang.org](https://try.vyperlang.org/), where the extension is installed and activated.
The source code for this website is available in the [GitHub repository](https://github.com/vyperlang/try.vyperlang.org).

#### Colab
It is also possible to run our plugin in [Google Colab](https://colab.research.google.com/).
To do this, you need to install the plugin by running the following commands:
```jupyter
!pip install titanoboa
%load_ext boa.ipython
```

#### IPython extensions

This activates the `%%vyper`, `%%contract` and `%%eval` magics.


### Basic tests
Expand Down
5 changes: 4 additions & 1 deletion boa/contracts/base_evm_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,10 @@ def __str__(self):
frame = self.stack_trace.last_frame
if hasattr(frame, "vm_error"):
err = frame.vm_error
err.args = (frame.pretty_vm_reason, *err.args[1:])
if not getattr(err, "_already_pretty", False):
# avoid double patching when str() is called more than once
setattr(err, "_already_pretty", True)
err.args = (frame.pretty_vm_reason, *err.args[1:])
else:
err = frame
return f"{err}\n\n{self.stack_trace}"
18 changes: 15 additions & 3 deletions boa/integrations/jupyter/jupyter.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,22 @@
/** Call the backend when the given function is called, handling errors */
const handleCallback = func => async (token, ...args) => {
if (!colab) {
// Check if the cell was already executed. In Colab, eval_js() doesn't replay.
// Check backend and whether cell was executed. In Colab, eval_js() doesn't replay.
const response = await fetch(`${base}/titanoboa_jupyterlab/callback/${token}`);
// !response.ok indicates the cell has already been executed
if (!response.ok) return;
if (response.status === 404 && response.headers.get('Content-Type') === 'application/json') {
return; // the cell has already been executed
}
if (!response.ok) {
const error = 'Could not connect to the titanoboa backend. Please make sure the Jupyter extension is installed by running the following command:';
const command = 'jupyter lab extension enable boa';
if (element) {
element.style.display = "block"; // show the output element in JupyterLab
element.innerHTML = `<h3 style="color: red">${error}</h3><pre>${command}</pre>`;
} else {
prompt(error, command);
}
return;
}
}

const body = stringify(await parsePromise(func(...args)));
Expand Down
4 changes: 2 additions & 2 deletions boa/interpret.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ def func():
return _disk_cache.caching_lookup(str((kwargs, source_code, deployer)), func)


def load(filename: str, *args, **kwargs) -> _Contract: # type: ignore
def load(filename: str | Path, *args, **kwargs) -> _Contract: # type: ignore
name = Path(filename).stem
# TODO: investigate if we can just put name in the signature
if "name" in kwargs:
Expand Down Expand Up @@ -154,7 +154,7 @@ def loads_abi(json_str: str, *args, name: str = None, **kwargs) -> ABIContractFa
def loads_partial(
source_code: str,
name: str = None,
filename: str = None,
filename: str | Path | None = None,
dedent: bool = True,
compiler_args: dict = None,
) -> _Deployer:
Expand Down
31 changes: 26 additions & 5 deletions boa/network.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ def __init__(

self.tx_settings = TransactionSettings()
self.capabilities = Capabilities(rpc)
self._suppress_debug_tt = False

@cached_property
def _rpc_has_snapshot(self):
Expand Down Expand Up @@ -420,6 +421,30 @@ def _warn_no_tracer():

return call_tracer

def suppress_debug_tt(self, new_value=True):
self._suppress_debug_tt = new_value

def _debug_tt(self, tx_hash):
if self._tracer is None:
return None
try:
return self._rpc.fetch_uncached(
"debug_traceTransaction", [tx_hash, self._tracer]
)
except RPCError as e:
if self._suppress_debug_tt:
warnings.warn(f"Couldn't get a trace for {tx_hash}!", stacklevel=3)
else:
warnings.warn(
f"Couldn't get a trace for {tx_hash}! If you want to"
" suppress this error, call `boa.env.suppress_debug_tt()`"
" first.",
stacklevel=3,
)
raise e

return None

def _get_nonce(self, addr):
return self._rpc.fetch("eth_getTransactionCount", [addr, "latest"])

Expand Down Expand Up @@ -481,11 +506,7 @@ def _send_txn(self, from_, to=None, gas=None, value=None, data=None):

receipt = self._rpc.wait_for_tx_receipt(tx_hash, self.tx_settings.poll_timeout)

trace = None
if self._tracer is not None:
trace = self._rpc.fetch_uncached(
"debug_traceTransaction", [tx_hash, self._tracer]
)
trace = self._debug_tt(tx_hash)

print(f"{tx_hash} mined in block {receipt['blockHash']}!")

Expand Down
3 changes: 2 additions & 1 deletion docs/source/testing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,8 @@ ipython Vyper Cells

Titanoboa supports ipython Vyper cells. This means that you can write Vyper code in a ipython/Jupyter Notebook environment and execute it as if it was a Python cell (the contract will be compiled instead, and a ``ContractFactory`` will be returned).

To enable this feature, execute ``%load_ext boa.ipython`` in a cell.
You can use Jupyter to execute titanoboa code in network mode from your browser using any wallet, using your wallet to sign transactions and call the RPC.
For a full example, please see `this example Jupyter notebook <../../examples/jupyter_browser_signer.ipynb>`_.

.. code-block:: python
Expand Down
130 changes: 11 additions & 119 deletions examples/jupyter_browser_signer.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -6,130 +6,22 @@
"id": "94744db8",
"metadata": {},
"outputs": [],
"source": [
"import boa; from boa.network import NetworkEnv"
]
"source": "%load_ext boa.ipython"
},
{
"cell_type": "code",
"execution_count": 2,
"id": "ff9dfb06",
"metadata": {},
"outputs": [],
"source": [
"%load_ext boa.ipython"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "9f241bf5",
"metadata": {},
"outputs": [
{
"data": {
"application/javascript": [
"\n",
"require.config({\n",
" paths: {\n",
" //ethers: \"https://cdnjs.cloudflare.com/ajax/libs/ethers/5.7.2/ethers.umd.min\"\n",
" ethers: \"https://cdnjs.cloudflare.com/ajax/libs/ethers/6.4.2/ethers.umd.min\"\n",
" }\n",
"});\n",
"\n",
"require(['ethers'], function(ethers) {\n",
" // Initialize ethers\n",
" let provider = new ethers.BrowserProvider(window.ethereum);\n",
"\n",
" // check that we have a signer for this account\n",
" Jupyter.notebook.kernel.comm_manager.register_target('get_signer', function(c, msg) {\n",
" // console.log(\"get_signer created\", c)\n",
" c.on_msg(function(msg) {\n",
" // console.log(\"get_signer called\", c)\n",
" let account = msg.content.data.account\n",
" provider.getSigner(account).then(signer => {\n",
" // console.log(\"success\", signer)\n",
" c.send({\"success\": signer});\n",
" }).catch(function(error) {\n",
" console.error(\"got error, percolating up:\", error);\n",
" c.send({\"error\": error});\n",
" });\n",
" });\n",
" });\n",
"\n",
" Jupyter.notebook.kernel.comm_manager.register_target(\"send_transaction\", function(c, msg) {\n",
" c.on_msg(function(msg) {\n",
" let tx_data = msg.content.data.transaction_data;\n",
" let account = msg.content.data.account\n",
" provider.getSigner(account).then(signer => {\n",
" signer.sendTransaction(tx_data).then(response => {\n",
" console.log(response);\n",
" c.send({\"success\": response});\n",
" }).catch(function(error) {\n",
" console.error(\"got error, percolating up:\", error);\n",
" c.send({\"error\": error});\n",
" });\n",
" }).catch(function(error) {\n",
" console.error(\"got error, percolating up:\", error);\n",
" c.send({\"error\": error});\n",
" });\n",
" });\n",
" });\n",
"});\n",
"\n",
"Jupyter.notebook.kernel.comm_manager.register_target(\"test_comm\", function(comm, msg) {\n",
" console.log(\"ENTER\", comm);\n",
" /*comm.on_close(function(msg) {\n",
" console.log(\"CLOSING\", msg);\n",
" });\n",
" */\n",
"\n",
" comm.on_msg(function(msg) {\n",
" console.log(\"ENTER 2\", comm);\n",
" console.log(\"ENTER 3\", msg.content.data);\n",
" setTimeout(() => {\n",
" comm.send({\"success\": \"hello\", \"echo\": msg.content.data});\n",
" comm.close();\n",
" console.log(comm);\n",
" }, 350);\n",
" });\n",
"});\n"
],
"text/plain": [
"<IPython.core.display.Javascript object>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"from boa.integrations.jupyter import BrowserSigner"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "814ff4f3",
"metadata": {},
"outputs": [],
"source": [
"boa.set_env(NetworkEnv(\"<rpc server address, e.g. an alchemy endpoint>\"))"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "a24872c9",
"metadata": {},
"outputs": [],
"execution_count": 2,
"source": [
"boa.env.add_account(BrowserSigner())"
]
"import boa\n",
"boa.set_browser_env() # this will use the browser signer and the browser RPC"
],
"id": "b724995f3df612f0"
},
{
"cell_type": "code",
"execution_count": 6,
"execution_count": 3,
"id": "1e98969d",
"metadata": {},
"outputs": [
Expand All @@ -139,7 +31,7 @@
"<boa.vyper.contract.VyperDeployer at 0x7f5150614a90>"
]
},
"execution_count": 6,
"execution_count": 3,
"metadata": {},
"output_type": "execute_result"
}
Expand All @@ -158,7 +50,7 @@
},
{
"cell_type": "code",
"execution_count": 7,
"execution_count": 4,
"id": "c5b60ed3",
"metadata": {},
"outputs": [
Expand All @@ -178,7 +70,7 @@
},
{
"cell_type": "code",
"execution_count": 8,
"execution_count": 5,
"id": "bdbfc09c",
"metadata": {},
"outputs": [
Expand All @@ -189,7 +81,7 @@
"<storage: totalSupply=1000, balances={'0x...<truncated>': 1000}>"
]
},
"execution_count": 8,
"execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
Expand Down
9 changes: 5 additions & 4 deletions tests/unitary/test_reverts.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,10 +156,11 @@ def add():
assert self.counter == 0
"""
)
try:
assert c.add()
except BoaError as e:
assert "<storage: counter=1>" in str(e)
with pytest.raises(BoaError) as context:
c.add()

assert "<storage: counter=1>" in str(context.value)
assert str(context.value).startswith("Revert(b'')")

assert 0 == c._storage.counter.get()
assert 0 == c.counter()
Expand Down

0 comments on commit 517e7df

Please sign in to comment.