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