diff --git a/Makefile b/Makefile
index b4bf940..09c7ca3 100644
--- a/Makefile
+++ b/Makefile
@@ -1,3 +1,4 @@
+.PHONY: docs
build:
hatch run dev:maturin develop
@@ -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
diff --git a/docs/evm.rst b/docs/evm.rst
index a3ecda4..58b502f 100644
--- a/docs/evm.rst
+++ b/docs/evm.rst
@@ -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()
diff --git a/docs/getstarted.rst b/docs/getstarted.rst
index c28c34e..7d14e9c 100644
--- a/docs/getstarted.rst
+++ b/docs/getstarted.rst
@@ -3,6 +3,9 @@
Getting Started
===============
+Install
+-------
+
Simular is available on `PyPi `_
It requires Python ``>=3.11``.
@@ -10,16 +13,12 @@ It requires Python ``>=3.11``.
>>> 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
@@ -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...``
\ No newline at end of file
+.. 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)
diff --git a/docs/index.rst b/docs/index.rst
index df76a2f..143abfe 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -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 `_.
+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...
diff --git a/docs/toc.rst b/docs/toc.rst
index 612f032..9f0bc78 100644
--- a/docs/toc.rst
+++ b/docs/toc.rst
@@ -1,14 +1,7 @@
-
-.. toctree::
- :maxdepth: 1
- :caption: Intro:
-
- getstarted
-
.. toctree::
:maxdepth: 2
- :caption: API:
+ getstarted
evm
contract
utils
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index 271e08f..afa1932 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -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 = [
@@ -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"
diff --git a/simular/contract.py b/simular/contract.py
index 92d6d9b..150cb97 100644
--- a/simular/contract.py
+++ b/simular/contract.py
@@ -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")
@@ -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):
@@ -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.
@@ -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)',
@@ -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
"""
diff --git a/simular/simular.pyi b/simular/simular.pyi
index 3bff40f..066aba0 100644
--- a/simular/simular.pyi
+++ b/simular/simular.pyi
@@ -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
@@ -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": ...
diff --git a/tests/conftest.py b/tests/conftest.py
index 46ff160..2d03841 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -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
diff --git a/tests/fixtures/BlockMeta.json b/tests/fixtures/BlockMeta.json
new file mode 100644
index 0000000..6c8d7f8
--- /dev/null
+++ b/tests/fixtures/BlockMeta.json
@@ -0,0 +1,109 @@
+{
+ "abi": [
+ {
+ "type": "function",
+ "name": "getMeta",
+ "inputs": [],
+ "outputs": [
+ {
+ "name": "",
+ "type": "uint256",
+ "internalType": "uint256"
+ },
+ {
+ "name": "",
+ "type": "uint256",
+ "internalType": "uint256"
+ }
+ ],
+ "stateMutability": "view"
+ }
+ ],
+ "bytecode": {
+ "object": "0x6080604052348015600f57600080fd5b50607c80601d6000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c8063a79af2ce14602d575b600080fd5b6040805142815243602082015281519081900390910190f3fea26469706673582212202c76d8081bf4b8745cf50463d5b4f48aadbd688456ec111406e9010a51d456ba64736f6c63430008150033",
+ "sourceMap": "65:175:22:-:0;;;;;;;;;;;;;;;;;;;",
+ "linkReferences": {}
+ },
+ "deployedBytecode": {
+ "object": "0x6080604052348015600f57600080fd5b506004361060285760003560e01c8063a79af2ce14602d575b600080fd5b6040805142815243602082015281519081900390910190f3fea26469706673582212202c76d8081bf4b8745cf50463d5b4f48aadbd688456ec111406e9010a51d456ba64736f6c63430008150033",
+ "sourceMap": "65:175:22:-:0;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;129:109;;;;201:15;188:25:35;;218:12:22;244:2:35;229:18;;222:34;129:109:22;;;;;;;;;;",
+ "linkReferences": {}
+ },
+ "methodIdentifiers": {
+ "getMeta()": "a79af2ce"
+ },
+ "rawMetadata": "{\"compiler\":{\"version\":\"0.8.21+commit.d9974bed\"},\"language\":\"Solidity\",\"output\":{\"abi\":[{\"inputs\":[],\"name\":\"getMeta\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"}],\"devdoc\":{\"kind\":\"dev\",\"methods\":{},\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{\"getMeta()\":{\"notice\":\"Used for testing the simulator\"}},\"version\":1}},\"settings\":{\"compilationTarget\":{\"src/BlockMeta.sol\":\"BlockMeta\"},\"evmVersion\":\"paris\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"ipfs\"},\"optimizer\":{\"enabled\":true,\"runs\":200},\"remappings\":[\":ds-test/=lib/forge-std/lib/ds-test/src/\",\":forge-std/=lib/forge-std/src/\",\":solmate/=lib/solmate/src/\"]},\"sources\":{\"src/BlockMeta.sol\":{\"keccak256\":\"0xbdc5d72b6d5eab867385d279eea3a36dc70fc21dd91750790e83f0e8724dc718\",\"license\":\"UNLICENSED\",\"urls\":[\"bzz-raw://c7122fdef2316f20f01744bc285901301dcbbbb3a3441c911ab4fbc4301356a2\",\"dweb:/ipfs/QmR9WJihJPtrXqmn1ifD8mVQxAvyBGr7MBSf4ojKY52adf\"]}},\"version\":1}",
+ "metadata": {
+ "compiler": {
+ "version": "0.8.21+commit.d9974bed"
+ },
+ "language": "Solidity",
+ "output": {
+ "abi": [
+ {
+ "inputs": [],
+ "stateMutability": "view",
+ "type": "function",
+ "name": "getMeta",
+ "outputs": [
+ {
+ "internalType": "uint256",
+ "name": "",
+ "type": "uint256"
+ },
+ {
+ "internalType": "uint256",
+ "name": "",
+ "type": "uint256"
+ }
+ ]
+ }
+ ],
+ "devdoc": {
+ "kind": "dev",
+ "methods": {},
+ "version": 1
+ },
+ "userdoc": {
+ "kind": "user",
+ "methods": {
+ "getMeta()": {
+ "notice": "Used for testing the simulator"
+ }
+ },
+ "version": 1
+ }
+ },
+ "settings": {
+ "remappings": [
+ "ds-test/=lib/forge-std/lib/ds-test/src/",
+ "forge-std/=lib/forge-std/src/",
+ "solmate/=lib/solmate/src/"
+ ],
+ "optimizer": {
+ "enabled": true,
+ "runs": 200
+ },
+ "metadata": {
+ "bytecodeHash": "ipfs"
+ },
+ "compilationTarget": {
+ "src/BlockMeta.sol": "BlockMeta"
+ },
+ "evmVersion": "paris",
+ "libraries": {}
+ },
+ "sources": {
+ "src/BlockMeta.sol": {
+ "keccak256": "0xbdc5d72b6d5eab867385d279eea3a36dc70fc21dd91750790e83f0e8724dc718",
+ "urls": [
+ "bzz-raw://c7122fdef2316f20f01744bc285901301dcbbbb3a3441c911ab4fbc4301356a2",
+ "dweb:/ipfs/QmR9WJihJPtrXqmn1ifD8mVQxAvyBGr7MBSf4ojKY52adf"
+ ],
+ "license": "UNLICENSED"
+ }
+ },
+ "version": 1
+ },
+ "id": 22
+}
\ No newline at end of file
diff --git a/tests/test_pyevm.py b/tests/test_pyevm.py
index 06960b7..2146e07 100644
--- a/tests/test_pyevm.py
+++ b/tests/test_pyevm.py
@@ -2,7 +2,7 @@
from eth_utils import to_wei
from eth_abi import decode
-from simular import PyEvm, PyAbi
+from simular import PyEvm, PyAbi, contract_from_raw_abi
def test_create_account_and_balance(evm, bob):
@@ -54,3 +54,24 @@ def test_contract_raw_interaction(evm, bob, kitchen_sink_json):
(enc1, _, _) = abi.encode_function("value", "()")
assert [1] == evm.call("value", "()", contract_address, abi)
+
+
+def test_advance_block(evm, bob, block_meta_json):
+ # simple contract that can return block.timestamp and number
+ evm.create_account(bob)
+
+ contract = contract_from_raw_abi(evm, block_meta_json)
+ contract.deploy(caller=bob)
+
+ ts1, bn1 = contract.getMeta.call()
+
+ assert bn1 == 1 # start at block 1
+
+ evm.advance_block()
+ evm.advance_block()
+ evm.advance_block()
+
+ ts2, bn2 = contract.getMeta.call()
+
+ assert bn2 == 4 # block advanced
+ assert ts2 == ts1 + 36 # timestamp advanced