Skip to content

Commit

Permalink
docs, tests
Browse files Browse the repository at this point in the history
  • Loading branch information
davebryson committed May 27, 2024
1 parent cc485ee commit a2ed7ad
Show file tree
Hide file tree
Showing 11 changed files with 338 additions and 41 deletions.
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.PHONY: docs

build:
hatch run dev:maturin develop
Expand All @@ -16,7 +17,7 @@ benchit:
hatch run dev:python bench/simple.py

docs:
hatch run:dev docs
hatch run dev:docs

shell:
hatch --env dev shell
17 changes: 15 additions & 2 deletions docs/evm.rst
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,21 @@ Example:
>>> evm.get_balance(alice)
1000000000000000000
.. note::
PyEvm as additional methods, but they are easier to use through :ref:`contract`
.. py:method:: advance_block(interval = None)
This method provides the ability to simulate the mining of blocks. It will advance
`block.number` and `block.timestamp`.

It's not necessary to call this method. However, some contracts may have logic
that need this information.

:param interval: (int) optional. set the time in seconds between blocks. Default is 12 seconds

Example:

.. code-block:: python
>>> evm.advance_block()
98 changes: 87 additions & 11 deletions docs/getstarted.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,22 @@
Getting Started
===============

Install
-------

Simular is available on `PyPi <https://pypi.org/project/simular-evm/>`_
It requires Python ``>=3.11``.

.. code-block:: bash
>>> pip install simular-evm
Here are a few examples of how to use simular. You can find more details
in the API section.

Here are a few examples of how to use simular.

Transfer Ether
--------------

In this example, we'll create 2 Ethereum accounts and show how to
transfer Ether between the accounts.
Create 2 Ethereum accounts and transfer Ether between the accounts.

.. code-block:: python
Expand All @@ -29,20 +28,97 @@ transfer Ether between the accounts.
# create an instance of the Evm
>>> evm = PyEvm()
# convert 1 ether to wei (1e18)
>>> one_ether = ether_to_wei(1)
# create a couple of accounts - one for Bob with and initial
# balance of 1 ether and one for Alice with no balance.
>>> bob = create_account(evm, value=ether_to_wei(1))
>>> bob = create_account(evm, value=one_ether)
>>> alice = create_account(evm)
# Bob transfers 1 ether to alice
>>> evm.transfer(bob, alice, ether_to_wei(1))
>>> evm.transfer(bob, alice, one_ether)
# check balances
>>> assert int(1e18) == evm.get_balance(alice)
>>> assert 0 == evm.get_balance(bob)
Interact with a Smart contract
------------------------------
Deploy and interact with a contract
-----------------------------------

Load an ERC20 contract and mint tokens.

``...todo...``
.. code-block:: python
# import the EVM and a few utility functions
from simular inport PyEvm, create_account, contract_from_abi_bytecode
def deploy_and_mint():
# Create an instance of the EVM
evm = PyEvm()
# Create accounts
alice = create_account(evm)
bob = create_account(evm)
# Load the contract.
# ABI and BYTECODE are the str versions of the ERC20 contract
# interface and compiled bytecode
erc20 = contract_from_abi_bytecode(evm, ABI, BYTECODE)
# Deploy the contract. Returns the contract's address
# The contract's constructor takes 3 arguments:
# name: MyCoin
# symbol: MYC
# decimals: 6
#
# 'caller' (bob) is the one deploying the contract. This
# translates to 'msg.sender'. And in the case of this contract,
# bob will be the 'owner'
contract_address = erc20.deploy("MyCoin", "MYC", 6, caller=bob)
print(contract_address)
# Let's check to see if it worked...
# Notice how the contract functions are added as attributes
# to the contract object.
#
# We use 'call' to make a read-only request
assert erc20.name.call() == "MyCoin"
assert erc20.decimals.call() == 6
assert erc20.owner.call() == bob
# Bob mints 10 tokens to alice.
# Again, 'mint' is a contract function. It's
# automatically attached to the erc20 contract
# object as an attribute.
# 'transact' is a write call to the contract (it will change state).
erc20.mint.transact(alice, 10, caller=bob)
# check balances and supply
assert 10 == erc20.balanceOf.call(alice)
assert 10 == erc20.totalSupply.call()
# Let's take a snapshot of the state of the EVM
# and use it again later to pre-populate the EVM:
snapshot = evm.create_snapshot()
# and save it to a file
with open('erc20snap.json', 'w') as f:
f.write(snapshot)
# ... later on, we can load this back into the EVM
with open('erc20snap.json') as f:
snapback = f.read()
# a new instance of the EVM
evm2 = PyEvm.from_snapshot(snapback)
# load the contract definitions
erc20back = contract_from_abi_bytecode(evm2, erc20abi, erc20bin)
# check the state was preserved in the snapshot
assert 10 == erc20back.balanceOf.call(alice)
14 changes: 5 additions & 9 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,24 @@
simular
=======

**Simular** is a Python API you can use to deploy and interact with Ethereum
smart contracts and an embedded Ethereum Virtual Machine (EVM). It creates a
Python wrapper around production grade Rust based Ethereum APIs making it very fast.

**Simular** is a Python API wrapped around a production grade Ethereum Virtual Machine (EVM). You can use to
locally deploy and interact with smart contracts, create accounts, transfer Ether, and much more.

How is it different than Brownie, Ganache, Anvil?

- It's only an EVM. It doesn't include blocks and mining
- It's only an EVM.
- No HTTP/JSON-RPC. You talk directly to the EVM (and it's fast)
- Full functionality: account transfers, contract interaction, and more.

The primary motivation for this work is to be able to model smart contract
interaction in an Agent Based Modeling environment
like `Mesa <https://mesa.readthedocs.io/en/main/>`_.
The primary motivation for this work is to have a lightweight, fast environment for simulating and modeling Ethereum applications.

Features
--------

- User-friendy Python API
- Run a local version with an in-memory database. Or copy (fork) state from a remote node.
- Parse Solidity ABI json files or define a specific set of functions using `human-readable` notation.
- Dump the current state of the EVM to json for future use in pre-populating EVM storage.
- User-friendy Python API


Standing on the shoulders of giants...
Expand Down
9 changes: 1 addition & 8 deletions docs/toc.rst
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@

.. toctree::
:maxdepth: 1
:caption: Intro:

getstarted

.. toctree::
:maxdepth: 2
:caption: API:

getstarted
evm
contract
utils
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ license = "Apache-2.0"
readme = "README.md"
homepage = "https://github.com/simular-fi/simular"
repository = "https://github.com/simular-fi/simular"
documentation = "https://github.com/simular-fi/simular/docs/"
documentation = "https://simular.readthedocs.io/en/latest/"
keywords = ["agent-based modeling", "ethereum", "solidity", "simulation"]
description = "smart-contract api and embedded ethereum virtual machine"
classifiers = [
Expand All @@ -45,5 +45,5 @@ docs = "sphinx-build -M html docs docs/build"

[project.urls]
Source = "https://github.com/simular-fi/simular"
Documentation = "https://simular-fi.github.io/simular"
Documentation = "https://simular.readthedocs.io/en/latest/"
Issues = "https://github.com/simular-fi/simular/issues"
12 changes: 8 additions & 4 deletions simular/contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@


def convert_for_soltypes(args: typing.Tuple):
"""
This converts `args` into a string for processing on the Rust side
"""
if not isinstance(args, tuple):
raise Exception("Expected tuple as arguments")

Expand All @@ -22,7 +25,8 @@ def clean(v):

class Function:
"""
Contains information needed to interact with a contract function
Contains information needed to interact with a contract function. This
is attached automatically to the Contract based on the ABI.
"""

def __init__(self, evm: PyEvm, abi: PyAbi, contract_address: str, name: str):
Expand Down Expand Up @@ -94,7 +98,7 @@ def transact(self, *args, caller: str = None, value: int = 0):
class Contract:
def __init__(self, evm: PyEvm, abi: PyAbi):
"""
Instantiate a contract from an ABI.
Instantiate a contract from an ABI with an EVM.
Maps contract functions to this class. Making function available
as attributes on the Contract.
Expand All @@ -107,7 +111,7 @@ def __init__(self, evm: PyEvm, abi: PyAbi):

def __getattr__(self, name: str) -> Function:
"""
Make solidity contract methods available as method calls. For a given function name,
Make solidity contract methods available as method calls. For a given function `name`,
return `Function`.
For example, if the ABI has the contract function 'function hello(uint256)',
Expand Down Expand Up @@ -140,8 +144,8 @@ def at(self, address: str) -> "Contract":
def deploy(self, *args, caller: str = None, value: int = 0) -> str:
"""
Deploy the contract, returning it's deployed address
- `caller`: the address of the requester...`msg.sender`
- `args`: a list of args (if any)
- `caller`: the address of the requester...`msg.sender`
- `value`: optional amount of Ether for the contract
Returns the address of the deployed contract
"""
Expand Down
83 changes: 80 additions & 3 deletions simular/simular.pyi
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from typing import Optional, Type
from typing import Optional, Type, List, Tuple

class PyEvm:
def __new__(cls: Type["PyEvm"]) -> "PyEvm":
"""
Create an instance of the Evm
Create an instance of the Evm using In-memory storage
"""

@staticmethod
Expand Down Expand Up @@ -65,4 +65,81 @@ class PyEvm:
"""

class PyAbi:
def from_full_json(cls: Type["PyAbi"]): ...
"""
Load, parse, and encode Solidity ABI information
"""

@staticmethod
def from_full_json(abi: str) -> "PyAbi":
"""
Load from a file that contains both ABI and bytecode information.
For example, the output from compiling a contract with Foundry
- `abi`: the str version of the compiled output file
"""

@staticmethod
def from_abi_bytecode(abi: str, data: Optional[bytes]) -> "PyAbi":
"""
Load the ABI and optionally the bytecode
- `abi`: just the abi information
- `data`: optionally the contract bytecode
"""

@staticmethod
def from_human_readable(values: List[str]) -> "PyAbi":
"""
Load from a list of contract function definitions.
- `values`: list of function definitions
For example: values = [
'function hello() returns (bool)',
'function add(uint256, uint256).
]
Would provide the ABI to encode the 'hello' and 'add' functions
"""

def has_function(self, name: str) -> bool:
"""
Does the contract have the function with the given name?
- `name`: the function name
"""

def has_fallback(self) -> bool:
"""
Does the contract have a fallback?
"""

def has_receive(self) -> bool:
"""
Does the contract have a receive?
"""

def bytecode(self) -> Optional[bytes]:
"""
Return the bytecode for the contract or None
"""

def encode_constructor(self, args: str) -> Tuple[bytes, bool]:
"""
ABI encode contract constructor. This is a low-level call.
See `Contract`
"""

def encode_function(
self, name: str, args: str
) -> Tuple[bytes, bool, "DynSolTypeWrapper"]:
"""
ABI encode a function. This is a low-level call.
See `Contract`
- `name`: name of the function
- `args`: arguments to the function
"""

class DynSolTypeWrapper:
def __new__(cls: Type["DynSolTypeWrapper"]) -> "DynSolTypeWrapper": ...
7 changes: 7 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,10 @@ def kitchen_sink_json():
with open(f"{PATH}/./fixtures/KitchenSink.json") as f:
rawabi = f.read()
return rawabi


@pytest.fixture
def block_meta_json():
with open(f"{PATH}/./fixtures/BlockMeta.json") as f:
rawabi = f.read()
return rawabi
Loading

0 comments on commit a2ed7ad

Please sign in to comment.