Repository: 0xSpaceShard/starknet-devnet (language: python)

Issue:
Feature request: Set block timestamp for testing purpose.
It would be really useful to be able to set block timestamp for testing purpose.
Currently it is really hard to test smart contracts that use `get_block_timestamp` because we don't have a way to mock the value in order to test different scenario.

Having this ability would help a lot. If you use `protostar` it is possible to mock block timestamp using [warp](https://docs.swmansion.com/protostar/docs/tutorials/guides/testing#warp) cheat code.
But if you use `nile` it is not possible to do the same because they use `starknet-devnet`.


Discussion / hints:
Thanks for the suggestion, this is under development.
> Thanks for the suggestion, this is under development.

Great, thanks.

Fix patch:
diff --git a/README.md b/README.md
index 0b0b5a969..292a2092a 100644
--- a/README.md
+++ b/README.md
@@ -16,6 +16,7 @@ Aims to mimic Starknet's Alpha testnet, but with simplified functionality.
 - [Block Explorer](#block-explorer)
 - [Lite Mode](#lite-mode)
 - [Restart](#restart)
+- [Advancing time](#advancing-time)
 - [Contract debugging](#contract-debugging)
 - [Devnet speed-up troubleshooting](#devnet-speed-up-troubleshooting)
 - [Development](#development)
@@ -68,6 +69,16 @@ optional arguments:
   --dump-path DUMP_PATH
                         Specify the path to dump to
   --dump-on DUMP_ON     Specify when to dump; can dump on: exit, transaction
+  --lite-mode           Applies all optimizations by disabling some
+                        features. These can be applied individually
+                        by using other flags instead of this one.
+  --lite-mode-block-hash
+                        Disables block hash calculation
+  --lite-mode-deploy-hash
+                        Disables deploy tx hash calculation
+  --start-time START_TIME
+                        Specify the start time of the genesis block
+                        in Unix time
 ```
 
 You can run `starknet-devnet` in a separate shell, or you can run it in background with `starknet-devnet &`.
@@ -245,6 +256,42 @@ Consider passing these CLI flags on Devnet startup:
 
 Devnet can be restarted by making a `POST /restart` request. All of the deployed contracts, blocks and storage updates will be restarted to the empty state. If you're using [the Hardhat plugin](https://github.com/Shard-Labs/starknet-hardhat-plugin#restart), run `await starknet.devnet.restart()`.
 
+## Advancing time
+
+Block timestamp can be manipulated by seting the exact time or seting the time offset. Timestamps methods won't generate a new block, but they will modify the time of the following blocks. All values should be set in [Unix time](https://en.wikipedia.org/wiki/Unix_time) and seconds.
+
+### Set time
+
+Sets the exact time of the next generated block. All blocks afterwards will keep a set offset.
+
+```
+POST /set_time
+{
+    "time": TIME_IN_SECONDS
+}
+```
+
+Warning: block time can be set in the past and lead to unexpected behaviour!
+
+### Increase time
+
+Increases the time offset for each generated block.
+
+```
+POST /increase_time
+{
+    "time": TIME_IN_SECONDS
+}
+```
+
+### Start time arg
+
+Devnet can be started with the `--start-time` argument.
+
+```
+starknet-devnet --start-time START_TIME_IN_SECONDS
+```
+
 ## Contract debugging
 
 If your contract is using `print` in cairo hints (it was compiled with the `--disable-hint-validation` flag), Devnet will output those lines together with its regular server output. To filter out just your debugging print lines, redirect stderr to /dev/null when starting Devnet:
diff --git a/starknet_devnet/block_info_generator.py b/starknet_devnet/block_info_generator.py
new file mode 100644
index 000000000..694c6f0b8
--- /dev/null
+++ b/starknet_devnet/block_info_generator.py
@@ -0,0 +1,49 @@
+"""
+Block info generator for the Starknet Devnet.
+"""
+
+import time
+
+from starkware.starknet.business_logic.state.state import BlockInfo
+from starkware.starknet.definitions.general_config import StarknetGeneralConfig
+
+def now() -> int:
+    """Get the current time in seconds."""
+    return int(time.time())
+
+class BlockInfoGenerator():
+    """Generator of BlockInfo objects with the correct timestamp"""
+
+    def __init__(self, start_time: int = None):
+        self.block_timestamp_offset = 0
+        self.next_block_start_time = start_time
+
+    def next_block(self, block_info: BlockInfo, general_config: StarknetGeneralConfig):
+        """
+        Returns the next block info with the correct timestamp
+        """
+        if self.next_block_start_time is None:
+            block_timestamp = now() + self.block_timestamp_offset
+        else:
+            block_timestamp = self.next_block_start_time
+            self.block_timestamp_offset = block_timestamp - now()
+            self.next_block_start_time = None
+
+        return BlockInfo(
+            gas_price=block_info.gas_price,
+            block_number=block_info.block_number,
+            block_timestamp=block_timestamp,
+            sequencer_address=general_config.sequencer_address
+        )
+
+    def increase_time(self, time_s: int):
+        """
+        Increases block timestamp offset
+        """
+        self.block_timestamp_offset += time_s
+
+    def set_next_block_time(self, time_s: int):
+        """
+        Sets the timestamp of next block
+        """
+        self.next_block_start_time = time_s
diff --git a/starknet_devnet/blocks.py b/starknet_devnet/blocks.py
new file mode 100644
index 000000000..82399380a
--- /dev/null
+++ b/starknet_devnet/blocks.py
@@ -0,0 +1,147 @@
+"""
+Class for generating and handling blocks
+"""
+
+from typing import Dict, Tuple
+
+from starkware.starknet.testing.state import StarknetState
+from starkware.starknet.services.api.feeder_gateway.block_hash import calculate_block_hash
+
+from .origin import Origin
+from .util import (
+    StarknetDevnetException,
+    TxStatus,
+    fixed_length_hex
+)
+from .transaction_wrapper import TransactionWrapper
+
+class DevnetBlocks():
+    """This class is used to store the generated blocks of the devnet."""
+
+    def __init__(self, origin: Origin, lite = False) -> None:
+        self.origin = origin
+        self.__num2block = {}
+        self.__state_updates = {}
+        self.__hash2num = {}
+        self.lite = lite
+
+    def __get_last_block(self):
+        """Returns the last block stored so far."""
+        number_of_blocks = self.get_number_of_blocks()
+        return self.get_by_number(number_of_blocks - 1)
+
+    def get_number_of_blocks(self) -> int:
+        """Returns the number of blocks stored so far."""
+        return len(self.__num2block) + self.origin.get_number_of_blocks()
+
+    def get_by_number(self, block_number: int) -> Dict:
+        """Returns the block whose block_number is provided"""
+        if block_number is None:
+            if self.__num2block:
+                return self.__get_last_block()
+            return self.origin.get_block_by_number(block_number)
+
+        if block_number < 0:
+            message = f"Block number must be a non-negative integer; got: {block_number}."
+            raise StarknetDevnetException(message=message)
+
+        if block_number >= self.get_number_of_blocks():
+            message = f"Block number too high. There are currently {len(self.__num2block)} blocks; got: {block_number}."
+            raise StarknetDevnetException(message=message)
+
+        if block_number in self.__num2block:
+            return self.__num2block[block_number]
+
+        return self.origin.get_block_by_number(block_number)
+
+    def get_by_hash(self, block_hash: str) -> Dict:
+        """
+        Returns the block with the given block hash.
+        """
+        numeric_hash = int(block_hash, 16)
+
+        if numeric_hash in self.__hash2num:
+            block_number = self.__hash2num[int(block_hash, 16)]
+            return self.get_by_number(block_number)
+
+        return self.origin.get_block_by_hash(block_hash)
+
+    def get_state_update(self, block_hash=None, block_number=None):
+        """
+        Returns state update for the provided block hash or block number.
+        It will return the last state update if block is not provided.
+        """
+        if block_hash:
+            numeric_hash = int(block_hash, 16)
+
+            if numeric_hash not in self.__hash2num:
+                return self.origin.get_state_update(block_hash=block_hash)
+
+            block_number = self.__hash2num[numeric_hash]
+
+        if block_number is not None:
+            if block_number not in self.__state_updates:
+                return self.origin.get_state_update(block_number=block_number)
+
+            return self.__state_updates[block_number]
+
+
+        return self.__state_updates.get(self.get_number_of_blocks() - 1) or self.origin.get_state_update()
+
+    async def generate(
+        self, tx_wrapper: TransactionWrapper, state: StarknetState,
+        state_root: bytes, state_update = None
+    ) -> Tuple[str, int]:
+        """
+        Generates a block and stores it to blocks and hash2block. The block contains just the passed transaction.
+        The `tx_wrapper.transaction` dict should contain a key `transaction`.
+        Returns (block_hash, block_number).
+        """
+        block_number = self.get_number_of_blocks()
+        timestamp = state.state.block_info.block_timestamp
+        signature = []
+        if "signature" in tx_wrapper.transaction["transaction"]:
+            signature = [int(sig_part, 16) for sig_part in tx_wrapper.transaction["transaction"]["signature"]]
+
+        parent_block_hash = self.__get_last_block()["block_hash"] if block_number else fixed_length_hex(0)
+        sequencer_address = state.general_config.sequencer_address
+        gas_price = state.general_config.min_gas_price
+
+        if self.lite:
+            block_hash = block_number
+        else:
+            block_hash = await calculate_block_hash(
+                general_config=state.general_config,
+                parent_hash=int(parent_block_hash, 16),
+                block_number=block_number,
+                global_state_root=state_root,
+                block_timestamp=timestamp,
+                tx_hashes=[int(tx_wrapper.transaction_hash, 16)],
+                tx_signatures=[signature],
+                event_hashes=[],
+                sequencer_address=state.general_config.sequencer_address
+            )
+
+        block_hash_hexed = fixed_length_hex(block_hash)
+        block = {
+            "block_hash": block_hash_hexed,
+            "block_number": block_number,
+            "gas_price": hex(gas_price),
+            "parent_block_hash": parent_block_hash,
+            "sequencer_address": hex(sequencer_address),
+            "state_root": state_root.hex(),
+            "status": TxStatus.ACCEPTED_ON_L2.name,
+            "timestamp": timestamp,
+            "transaction_receipts": [tx_wrapper.get_receipt_block_variant()],
+            "transactions": [tx_wrapper.transaction["transaction"]],
+        }
+
+        self.__num2block[block_number] = block
+        self.__hash2num[block_hash] = block_number
+
+        if state_update is not None:
+            state_update["block_hash"] = hex(block_hash)
+
+        self.__state_updates[block_number] = state_update
+
+        return block_hash_hexed, block_number
diff --git a/starknet_devnet/blueprints/base.py b/starknet_devnet/blueprints/base.py
index bd5f1d477..4172c2f5d 100644
--- a/starknet_devnet/blueprints/base.py
+++ b/starknet_devnet/blueprints/base.py
@@ -1,7 +1,7 @@
 """
 Base routes
 """
-from flask import Blueprint, Response, request
+from flask import Blueprint, Response, request, jsonify
 
 from starknet_devnet.state import state
 from starknet_devnet.util import StarknetDevnetException
@@ -30,3 +30,37 @@ def dump():
 
     state.dumper.dump(dump_path)
     return Response(status=200)
+
+def validate_time_value(value: int):
+    """Validates the time value"""
+    if value is None:
+        raise StarknetDevnetException(message="Time value must be provided.", status_code=400)
+
+    if not isinstance(value, int):
+        raise StarknetDevnetException(message="Time value must be an integer.", status_code=400)
+
+    if value < 0:
+        raise StarknetDevnetException(message="Time value must be greater than 0.", status_code=400)
+
+
+@base.route("/increase_time", methods=["POST"])
+def increase_time():
+    """Increases the block timestamp offset"""
+    request_dict = request.json or {}
+    time_s = request_dict.get("time")
+    validate_time_value(time_s)
+
+    state.starknet_wrapper.increase_block_time(time_s)
+
+    return jsonify({"timestamp_increased_by": time_s})
+
+@base.route("/set_time", methods=["POST"])
+def set_time():
+    """Sets the block timestamp offset"""
+    request_dict = request.json or {}
+    time_s = request_dict.get("time")
+    validate_time_value(time_s)
+
+    state.starknet_wrapper.set_block_time(time_s)
+
+    return jsonify({"next_block_timestamp": time_s})
diff --git a/starknet_devnet/blueprints/feeder_gateway.py b/starknet_devnet/blueprints/feeder_gateway.py
index 413239b1f..ac5d0e612 100644
--- a/starknet_devnet/blueprints/feeder_gateway.py
+++ b/starknet_devnet/blueprints/feeder_gateway.py
@@ -64,9 +64,9 @@ async def get_block():
     _check_block_arguments(block_hash, block_number)
 
     if block_hash is not None:
-        result_dict = state.starknet_wrapper.get_block_by_hash(block_hash)
+        result_dict = state.starknet_wrapper.blocks.get_by_hash(block_hash)
     else:
-        result_dict = state.starknet_wrapper.get_block_by_number(block_number)
+        result_dict = state.starknet_wrapper.blocks.get_by_number(block_number)
 
     return jsonify(result_dict)
 
@@ -79,7 +79,7 @@ def get_code():
     _check_block_hash(request.args)
 
     contract_address = request.args.get("contractAddress", type=custom_int)
-    result_dict = state.starknet_wrapper.get_code(contract_address)
+    result_dict = state.starknet_wrapper.contracts.get_code(contract_address)
     return jsonify(result_dict)
 
 @feeder_gateway.route("/get_full_contract", methods=["GET"])
@@ -91,7 +91,7 @@ def get_full_contract():
 
     contract_address = request.args.get("contractAddress", type=custom_int)
 
-    result_dict = state.starknet_wrapper.get_full_contract(contract_address)
+    result_dict = state.starknet_wrapper.contracts.get_full_contract(contract_address)
 
     return jsonify(result_dict)
 
@@ -113,8 +113,8 @@ def get_transaction_status():
     """
 
     transaction_hash = request.args.get("transactionHash")
-    ret = state.starknet_wrapper.get_transaction_status(transaction_hash)
-    return jsonify(ret)
+    transaction_status = state.starknet_wrapper.transactions.get_transaction_status(transaction_hash)
+    return jsonify(transaction_status)
 
 @feeder_gateway.route("/get_transaction", methods=["GET"])
 def get_transaction():
@@ -123,8 +123,8 @@ def get_transaction():
     """
 
     transaction_hash = request.args.get("transactionHash")
-    ret = state.starknet_wrapper.get_transaction(transaction_hash)
-    return jsonify(ret)
+    transaction = state.starknet_wrapper.transactions.get_transaction(transaction_hash)
+    return jsonify(transaction)
 
 @feeder_gateway.route("/get_transaction_receipt", methods=["GET"])
 def get_transaction_receipt():
@@ -133,8 +133,8 @@ def get_transaction_receipt():
     """
 
     transaction_hash = request.args.get("transactionHash")
-    ret = state.starknet_wrapper.get_transaction_receipt(transaction_hash)
-    return jsonify(ret)
+    transaction_receipt = state.starknet_wrapper.transactions.get_transaction_receipt(transaction_hash)
+    return jsonify(transaction_receipt)
 
 @feeder_gateway.route("/get_transaction_trace", methods=["GET"])
 def get_transaction_trace():
@@ -143,8 +143,7 @@ def get_transaction_trace():
     """
 
     transaction_hash = request.args.get("transactionHash")
-
-    transaction_trace = state.starknet_wrapper.get_transaction_trace(transaction_hash)
+    transaction_trace = state.starknet_wrapper.transactions.get_transaction_trace(transaction_hash)
 
     return jsonify(transaction_trace)
 
@@ -158,7 +157,7 @@ def get_state_update():
     block_hash = request.args.get("blockHash")
     block_number = request.args.get("blockNumber", type=custom_int)
 
-    state_update = state.starknet_wrapper.get_state_update(block_hash=block_hash, block_number=block_number)
+    state_update = state.starknet_wrapper.blocks.get_state_update(block_hash=block_hash, block_number=block_number)
 
     return jsonify(state_update)
 
diff --git a/starknet_devnet/contracts.py b/starknet_devnet/contracts.py
new file mode 100644
index 000000000..85fece555
--- /dev/null
+++ b/starknet_devnet/contracts.py
@@ -0,0 +1,57 @@
+"""
+Class for storing and handling contracts
+"""
+
+from .origin import Origin
+from .util import (
+    StarknetDevnetException,
+    fixed_length_hex
+)
+from .contract_wrapper import ContractWrapper
+
+class DevnetContracts:
+    """
+    This class is used to store the deployed contracts of the devnet.
+    """
+
+    def __init__(self, origin: Origin):
+        self.origin = origin
+        self.__intstances = {}
+
+    def store(self, address: str, contract: ContractWrapper) -> None:
+        """
+        Store the contract wrapper.
+        """
+        self.__intstances[address] = contract
+
+    def is_deployed(self, address: str) -> bool:
+        """
+        Check if the contract is deployed.
+        """
+        return address in self.__intstances
+
+    def get_by_address(self, address: str) -> ContractWrapper:
+        """
+        Get the contract wrapper by address.
+        """
+        if not self.is_deployed(address):
+            message = f"No contract at the provided address ({fixed_length_hex(address)})."
+            raise StarknetDevnetException(message=message)
+
+        return self.__intstances[address]
+
+    def get_code(self, address: str) -> str:
+        """
+        Get the contract code by address.
+        """
+        if not self.is_deployed(address):
+            return self.origin.get_code(address)
+
+        return self.__intstances[address].code
+
+    def get_full_contract(self, address: str) -> ContractWrapper:
+        """
+        Get the contract wrapper by address.
+        """
+        contract = self.get_by_address(address)
+        return contract.contract_definition
diff --git a/starknet_devnet/postman_wrapper.py b/starknet_devnet/postman_wrapper.py
index a7469842f..4b46f7995 100644
--- a/starknet_devnet/postman_wrapper.py
+++ b/starknet_devnet/postman_wrapper.py
@@ -1,14 +1,104 @@
 """
 This module wraps the usage of Postman for L1 <> L2 interaction.
 """
+import json
+
 from abc import ABC, abstractmethod
 from web3 import HTTPProvider, Web3
 
 from starkware.solidity.utils import load_nearby_contract
 from starkware.starknet.testing.postman import Postman
+from starkware.starknet.testing.starknet import Starknet
 from starkware.eth.eth_test_utils import EthAccount, EthContract
 
 from .constants import L1_MESSAGE_CANCELLATION_DELAY, TIMEOUT_FOR_WEB3_REQUESTS
+from .util import fixed_length_hex, StarknetDevnetException
+
+
+class DevnetL1L2:
+    """
+    This class is used to interact with the L1 <> L2 interaction.
+    """
+
+    def __init__(self) -> None:
+        self.__l1_provider = None
+        self.__postman_wrapper = None
+
+    def __parse_l1_l2_messages(self, l1_raw_messages, l2_raw_messages) -> dict:
+        """Converts some of the values in the dictionaries from integer to hex and keys to snake_case."""
+
+        for message in l1_raw_messages:
+            message["args"]["selector"] = hex(message["args"]["selector"])
+            message["args"]["to_address"] = fixed_length_hex(message["args"].pop("toAddress")) # L2 addresses need the leading 0
+            message["args"]["from_address"] = message["args"].pop("fromAddress")
+            message["args"]["payload"] = [hex(val) for val in message["args"]["payload"]]
+
+            # change case to snake_case
+            message["transaction_hash"] = message.pop("transactionHash")
+            message["block_hash"] = message.pop("blockHash")
+            message["block_number"] = message.pop("blockNumber")
+            message["transaction_index"] = message.pop("transactionIndex")
+            message["log_index"] = message.pop("logIndex")
+
+        l2_messages = []
+        for message in l2_raw_messages:
+            new_message = {
+                "from_address": fixed_length_hex(message.from_address), # L2 addresses need the leading 0
+                "payload": [hex(val) for val in message.payload],
+                "to_address": hex(message.to_address)
+            }
+            l2_messages.append(new_message)
+
+        return {
+            "l1_provider": self.__l1_provider,
+            "consumed_messages": {
+                "from_l1": l1_raw_messages,
+                "from_l2": l2_messages
+            }
+        }
+
+    def load_l1_messaging_contract(
+        self, starknet: Starknet, network_url: str, contract_address: str, network_id: str
+    ) -> dict:
+        """Creates a Postman Wrapper instance and loads an already deployed Messaging contract in the L1 network"""
+
+        # If no L1 network ID provided, will use a local testnet instance
+        if network_id is None or network_id == "local":
+            try:
+                starknet.state.l2_to_l1_messages_log.clear()
+                self.__postman_wrapper = LocalPostmanWrapper(network_url)
+                self.__postman_wrapper.load_mock_messaging_contract_in_l1(starknet,contract_address)
+            except Exception as error:
+                message = """Unable to load the Starknet Messaging contract in a local testnet instance.
+Make sure you have a local testnet instance running at the provided network url ({network_url}),
+and that the Messaging Contract is deployed at the provided address ({contract_address})."""
+                raise StarknetDevnetException(message=message) from error
+        else:
+            message = "L1 interaction is only usable with a local running local testnet instance."
+            raise StarknetDevnetException(message=message)
+
+        self.__l1_provider = network_url
+
+        return {
+            "l1_provider": network_url,
+            "address": self.__postman_wrapper.mock_starknet_messaging_contract.address
+        }
+
+    async def flush(self, state) -> dict:
+        """Handles all pending L1 <> L2 messages and sends them to the other layer. """
+
+        if self.__postman_wrapper is None:
+            return {}
+
+        postman = self.__postman_wrapper.postman
+
+        l1_to_l2_messages = json.loads(Web3.toJSON(self.__postman_wrapper.l1_to_l2_message_filter.get_new_entries()))
+        l2_to_l1_messages = state.l2_to_l1_messages_log[postman.n_consumed_l2_to_l1_messages :]
+
+        await self.__postman_wrapper.flush()
+
+        return self.__parse_l1_l2_messages(l1_to_l2_messages, l2_to_l1_messages)
+
 
 
 class PostmanWrapper(ABC):
@@ -52,5 +142,5 @@ def load_mock_messaging_contract_in_l1(self, starknet, contract_address):
             w3_contract = self.web3.eth.contract(abi=abi,address=address)
             self.mock_starknet_messaging_contract = EthContract(self.web3,address,w3_contract,abi,self.eth_account)
 
-        self.postman = Postman(self.mock_starknet_messaging_contract,starknet)
+        self.postman = Postman(self.mock_starknet_messaging_contract, starknet)
         self.l1_to_l2_message_filter = self.mock_starknet_messaging_contract.w3_contract.events.LogMessageToL2.createFilter(fromBlock="latest")
diff --git a/starknet_devnet/server.py b/starknet_devnet/server.py
index daa460dd6..6d63d3042 100644
--- a/starknet_devnet/server.py
+++ b/starknet_devnet/server.py
@@ -16,6 +16,7 @@
 from .blueprints.postman import postman
 from .util import DumpOn, parse_args
 from .state import state
+from .starknet_wrapper import DevnetConfig
 
 app = Flask(__name__)
 CORS(app)
@@ -40,15 +41,23 @@ def main():
         except (FileNotFoundError, pickle.UnpicklingError):
             sys.exit(f"Error: Cannot load from {args.load_path}. Make sure the file exists and contains a Devnet dump.")
 
+    if args.lite_mode:
+        config = DevnetConfig(
+            lite_mode_block_hash=True,
+            lite_mode_deploy_hash=True
+        )
+    else:
+        config = DevnetConfig(
+            lite_mode_block_hash=args.lite_mode_block_hash,
+            lite_mode_deploy_hash=args.lite_mode_deploy_hash
+        )
+
+    state.starknet_wrapper.set_config(config)
     state.dumper.dump_path = args.dump_path
     state.dumper.dump_on = args.dump_on
 
-    if args.lite_mode:
-        state.starknet_wrapper.lite_mode_block_hash = True
-        state.starknet_wrapper.lite_mode_deploy_hash = True
-    else:
-        state.starknet_wrapper.lite_mode_block_hash = args.lite_mode_block_hash
-        state.starknet_wrapper.lite_mode_deploy_hash = args.lite_mode_deploy_hash
+    if args.start_time is not None:
+        state.starknet_wrapper.set_block_time(args.start_time)
 
     try:
         meinheld.listen((args.host, args.port))
diff --git a/starknet_devnet/starknet_wrapper.py b/starknet_devnet/starknet_wrapper.py
index a84f500cd..2348ce8e6 100644
--- a/starknet_devnet/starknet_wrapper.py
+++ b/starknet_devnet/starknet_wrapper.py
@@ -3,37 +3,41 @@
 starkware.starknet.testing.starknet.Starknet.
 """
 
-import json
-import time
+import dataclasses
 from copy import deepcopy
 from typing import Dict
-from web3 import Web3
 
 import dill as pickle
 from starkware.starknet.business_logic.internal_transaction import InternalInvokeFunction
-from starkware.starknet.business_logic.state.state import CarriedState, BlockInfo
-from starkware.starknet.definitions.transaction_type import TransactionType
+from starkware.starknet.business_logic.state.state import CarriedState
 from starkware.starknet.services.api.gateway.contract_address import calculate_contract_address
-from starkware.starknet.services.api.gateway.transaction import InvokeFunction, Deploy, Transaction
+from starkware.starknet.services.api.gateway.transaction import InvokeFunction, Deploy
 from starkware.starknet.testing.starknet import Starknet
-from starkware.starknet.testing.objects import StarknetTransactionExecutionInfo
 from starkware.starkware_utils.error_handling import StarkException
-from starkware.starknet.services.api.feeder_gateway.block_hash import calculate_block_hash
 from starkware.starknet.business_logic.transaction_fee import calculate_tx_fee_by_cairo_usage
 
 from .origin import NullOrigin, Origin
 from .general_config import DEFAULT_GENERAL_CONFIG
 from .util import (
     StarknetDevnetException, TxStatus, DummyExecutionInfo,
-    fixed_length_hex, enable_pickling, generate_state_update
+    enable_pickling, generate_state_update
 )
 from .contract_wrapper import ContractWrapper, call_internal_tx
-from .transaction_wrapper import TransactionWrapper, DeployTransactionWrapper, InvokeTransactionWrapper
-from .postman_wrapper import LocalPostmanWrapper
-from .constants import FAILURE_REASON_KEY
+from .transaction_wrapper import DeployTransactionWrapper, InvokeTransactionWrapper, TransactionWrapper
+from .postman_wrapper import DevnetL1L2
+from .transactions import DevnetTransactions
+from .contracts import DevnetContracts
+from .blocks import DevnetBlocks
+from .block_info_generator import BlockInfoGenerator
 
 enable_pickling()
 
+@dataclasses.dataclass
+class DevnetConfig:
+    """Configuration for the devnet."""
+    lite_mode_block_hash: bool = False
+    lite_mode_deploy_hash: bool = False
+
 #pylint: disable=too-many-instance-attributes
 class StarknetWrapper:
     """
@@ -41,39 +45,25 @@ class StarknetWrapper:
     contract states, transactions, blocks, storages.
     """
 
-    def __init__(self):
+    def __init__(self, config: DevnetConfig):
         self.origin: Origin = NullOrigin()
         """Origin chain that this devnet was forked from."""
 
-        self.__address2contract_wrapper: Dict[int, ContractWrapper] = {}
-        """Maps contract address to contract wrapper."""
-
-        self.__transaction_wrappers: Dict[int, TransactionWrapper] = {}
-        """Maps transaction hash to transaction wrapper."""
+        self.transactions = DevnetTransactions(self.origin)
 
-        self.__hash2block: Dict[int, dict] = {}
-        """Maps block hash to block."""
+        self.contracts = DevnetContracts(self.origin)
 
-        self.__num2block: Dict[int, Dict] = {}
-        """Maps block number to block (one transaction per block); holds only own blocks."""
+        self.blocks = DevnetBlocks(self.origin, lite=config.lite_mode_block_hash)
 
-        self.__hash2state_update: Dict[int, dict] = {}
-        """Maps block hash to state update"""
+        self.l1l2 = DevnetL1L2()
 
         self.__starknet = None
 
         self.__current_carried_state = None
 
-        self.__postman_wrapper = None
-
-        self.__l1_provider = None
-        """Saves the L1 URL being used for L1 <> L2 communication."""
+        self.config = config
 
-        self.__last_state_update = None
-
-        self.lite_mode_block_hash = False
-
-        self.lite_mode_deploy_hash = False
+        self.block_info_generator = BlockInfoGenerator()
 
     @staticmethod
     def load(path: str) -> "StarknetWrapper":
@@ -85,13 +75,14 @@ async def __preserve_current_state(self, state: CarriedState):
         self.__current_carried_state = deepcopy(state)
         self.__current_carried_state.shared_state = state.shared_state
 
-    async def get_starknet(self):
+    async def __get_starknet(self):
         """
         Returns the underlying Starknet instance, creating it first if necessary.
         """
         if not self.__starknet:
             self.__starknet = await Starknet.empty(general_config=DEFAULT_GENERAL_CONFIG)
             await self.__preserve_current_state(self.__starknet.state.state)
+
         return self.__starknet
 
     async def __get_state(self):
@@ -99,21 +90,19 @@ async def __get_state(self):
         Returns the StarknetState of the underlyling Starknet instance,
         creating the instance first if necessary.
         """
-        starknet = await self.get_starknet()
+        starknet = await self.__get_starknet()
         return starknet.state
 
     async def __update_state(self):
-        if not self.lite_mode_block_hash:
+        if not self.config.lite_mode_block_hash:
             previous_state = self.__current_carried_state
             assert previous_state is not None
             current_carried_state = (await self.__get_state()).state
             state = await self.__get_state()
 
-            current_carried_state.block_info = BlockInfo(
-                block_number=current_carried_state.block_info.block_number,
-                gas_price=current_carried_state.block_info.gas_price,
-                block_timestamp=int(time.time()),
-                sequencer_address=state.general_config.sequencer_address
+            current_carried_state.block_info = self.block_info_generator.next_block(
+                block_info=current_carried_state.block_info,
+                general_config=state.general_config
             )
 
             updated_shared_state = await current_carried_state.shared_state.apply_state_updates(
@@ -124,21 +113,43 @@ async def __update_state(self):
             self.__starknet.state.state.shared_state = updated_shared_state
             await self.__preserve_current_state(self.__starknet.state.state)
 
-            self.__last_state_update = generate_state_update(previous_state, current_carried_state)
+            return generate_state_update(previous_state, current_carried_state)
 
     async def __get_state_root(self):
         state = await self.__get_state()
         return state.state.shared_state.contract_states.root
 
-    def __is_contract_deployed(self, address: int) -> bool:
-        return address in self.__address2contract_wrapper
+    async def __store_transaction(
+        self, tx_wrapper: TransactionWrapper, state_update: Dict, error_message: str=None
+    ) -> None:
+        """
+        Stores the provided data as a deploy transaction in `self.transactions`.
+        Generates a new block
+        """
 
-    def __get_contract_wrapper(self, address: int) -> ContractWrapper:
-        if not self.__is_contract_deployed(address):
-            message = f"No contract at the provided address ({fixed_length_hex(address)})."
-            raise StarknetDevnetException(message=message)
+        if tx_wrapper.transaction["status"] == TxStatus.REJECTED:
+            assert error_message, "error_message must be present if tx rejected"
+            tx_wrapper.set_failure_reason(error_message)
+        else:
+            state = await self.__get_state()
+            state_root = await self.__get_state_root()
+
+            block_hash, block_number = await self.blocks.generate(
+                tx_wrapper,
+                state,
+                state_root,
+                state_update=state_update,
+            )
+            tx_wrapper.set_block_data(block_hash, block_number)
+
+        self.transactions.store(tx_wrapper)
 
-        return self.__address2contract_wrapper[address]
+    def set_config(self, config: DevnetConfig):
+        """
+        Sets the configuration of the devnet.
+        """
+        self.config = config
+        self.blocks.lite = config.lite_mode_block_hash
 
     async def deploy(self, deploy_transaction: Deploy):
         """
@@ -148,12 +159,12 @@ async def deploy(self, deploy_transaction: Deploy):
 
         state = await self.__get_state()
         contract_definition = deploy_transaction.contract_definition
-        if self.lite_mode_deploy_hash:
-            tx_hash = len(self.__transaction_wrappers)
+        if self.config.lite_mode_deploy_hash:
+            tx_hash = self.transactions.get_count()
         else:
             tx_hash = deploy_transaction.calculate_hash(state.general_config)
 
-        starknet = await self.get_starknet()
+        starknet = await self.__get_starknet()
 
         try:
             contract = await starknet.deploy(
@@ -166,12 +177,13 @@ async def deploy(self, deploy_transaction: Deploy):
             error_message = None
             status = TxStatus.ACCEPTED_ON_L2
 
-            self.__address2contract_wrapper[contract.contract_address] = ContractWrapper(contract, contract_definition)
-            await self.__update_state()
+            self.contracts.store(contract.contract_address, ContractWrapper(contract, contract_definition))
+            state_update = await self.__update_state()
         except StarkException as err:
             error_message = err.message
             status = TxStatus.REJECTED
             execution_info = DummyExecutionInfo()
+            state_update = None
 
             contract_address = calculate_contract_address(
                 caller_address=0,
@@ -180,14 +192,19 @@ async def deploy(self, deploy_transaction: Deploy):
                 contract_definition=deploy_transaction.contract_definition
             )
 
-        await self.__store_transaction(
+        tx_wrapper = DeployTransactionWrapper(
             transaction=deploy_transaction,
             contract_address=contract_address,
             tx_hash=tx_hash,
             status=status,
             execution_info=execution_info,
+            contract_hash=state.state.contract_states[contract_address].state.contract_hash,
+        )
+
+        await self.__store_transaction(
+            tx_wrapper=tx_wrapper,
+            state_update=state_update,
             error_message=error_message,
-            contract_hash=state.state.contract_states[contract_address].state.contract_hash
         )
 
         return contract_address, tx_hash
@@ -205,7 +222,7 @@ async def invoke(self, transaction: InvokeFunction):
                     message = f"Actual fee exceeded max fee.\n{actual_fee} > {invoke_transaction.max_fee}"
                     raise StarknetDevnetException(message=message)
 
-            contract_wrapper = self.__get_contract_wrapper(invoke_transaction.contract_address)
+            contract_wrapper = self.contracts.get_by_address(invoke_transaction.contract_address)
             adapted_result, execution_info = await contract_wrapper.invoke(
                 entry_point_selector=invoke_transaction.entry_point_selector,
                 calldata=invoke_transaction.calldata,
@@ -215,19 +232,19 @@ async def invoke(self, transaction: InvokeFunction):
             )
             status = TxStatus.ACCEPTED_ON_L2
             error_message = None
-            await self.__update_state()
+            state_update = await self.__update_state()
         except StarkException as err:
             error_message = err.message
             status = TxStatus.REJECTED
             execution_info = DummyExecutionInfo()
             adapted_result = []
+            state_update = None
+
+        tx_wrapper = InvokeTransactionWrapper(invoke_transaction, status, execution_info)
 
         await self.__store_transaction(
-            transaction=invoke_transaction,
-            contract_address=transaction.contract_address,
-            tx_hash=invoke_transaction.hash_value,
-            status=status,
-            execution_info=execution_info,
+            tx_wrapper=tx_wrapper,
+            state_update=state_update,
             error_message=error_message
         )
 
@@ -235,7 +252,7 @@ async def invoke(self, transaction: InvokeFunction):
 
     async def call(self, transaction: InvokeFunction):
         """Perform call according to specifications in `transaction`."""
-        contract_wrapper = self.__get_contract_wrapper(transaction.contract_address)
+        contract_wrapper = self.contracts.get_by_address(transaction.contract_address)
 
         adapted_result = await contract_wrapper.call(
             entry_point_selector=transaction.entry_point_selector,
@@ -247,201 +264,6 @@ async def call(self, transaction: InvokeFunction):
 
         return { "result": adapted_result }
 
-    def get_transaction_status(self, transaction_hash: str):
-        """Returns the status of the transaction identified by `transaction_hash`."""
-
-        tx_hash_int = int(transaction_hash, 16)
-        if tx_hash_int in self.__transaction_wrappers:
-            transaction_wrapper = self.__transaction_wrappers[tx_hash_int]
-            transaction = transaction_wrapper.transaction
-
-            # the transaction status object only needs 1-3 elements from the transaction_wrapper object
-            ret = {
-                # "tx_status" always exists
-                "tx_status": transaction["status"]
-            }
-
-            # "block_hash" will only exist after transaction enters ACCEPTED_ON_L2
-            if "block_hash" in transaction:
-                ret["block_hash"] = transaction["block_hash"]
-
-            # "tx_failure_reason" will only exist if the transaction was rejected.
-            # the key in the transaction_wrapper object is "transaction_failure_reason"
-            # first it must be checked if the object contains an element with that key
-            if FAILURE_REASON_KEY in transaction:
-                ret["tx_failure_reason"] = transaction[FAILURE_REASON_KEY]
-
-            return ret
-
-        return self.origin.get_transaction_status(transaction_hash)
-
-    def get_transaction(self, transaction_hash: str):
-        """Returns the transaction identified by `transaction_hash`."""
-
-        tx_hash_int = int(transaction_hash,16)
-        if tx_hash_int in self.__transaction_wrappers:
-            return self.__transaction_wrappers[tx_hash_int].transaction
-
-        return self.origin.get_transaction(transaction_hash)
-
-    def get_transaction_receipt(self, transaction_hash: str):
-        """Returns the transaction receipt of the transaction identified by `transaction_hash`."""
-
-        tx_hash_int = int(transaction_hash, 16)
-        if tx_hash_int in self.__transaction_wrappers:
-            return self.__transaction_wrappers[tx_hash_int].receipt
-
-        return self.origin.get_transaction_receipt(transaction_hash)
-
-    def get_transaction_trace(self, transaction_hash:str):
-        """Returns the transaction trace of the tranasction indetified by `transaction_hash`"""
-
-        tx_hash_int = int(transaction_hash, 16)
-        if tx_hash_int in self.__transaction_wrappers:
-            status = self.__transaction_wrappers[tx_hash_int].transaction["status"]
-            transaction_wrapper = self.__transaction_wrappers[tx_hash_int]
-
-            if not hasattr(transaction_wrapper, "trace"):
-                raise StarknetDevnetException(
-                    f"Transaction corresponding to hash {tx_hash_int} has no trace; status: {status}."
-                )
-
-            return transaction_wrapper.trace
-
-        return self.origin.get_transaction_trace(transaction_hash)
-
-    def get_number_of_blocks(self) -> int:
-        """Returns the number of blocks stored so far."""
-        return len(self.__num2block) + self.origin.get_number_of_blocks()
-
-    async def __generate_block(self, tx_wrapper: TransactionWrapper):
-        """
-        Generates a block and stores it to blocks and hash2block. The block contains just the passed transaction.
-        The `tx_wrapper.transaction` dict should contain a key `transaction`.
-        Returns (block_hash, block_number).
-        """
-
-        state = await self.__get_state()
-        state_root = await self.__get_state_root()
-        block_number = self.get_number_of_blocks()
-        timestamp = state.state.block_info.block_timestamp
-        signature = []
-        if "signature" in tx_wrapper.transaction["transaction"]:
-            signature = [int(sig_part, 16) for sig_part in tx_wrapper.transaction["transaction"]["signature"]]
-        sequencer_address = state.general_config.sequencer_address
-        gas_price = state.general_config.min_gas_price
-
-        parent_block_hash = self.__get_last_block()["block_hash"] if block_number else fixed_length_hex(0)
-
-        if self.lite_mode_block_hash:
-            block_hash = block_number
-        else:
-            block_hash = await calculate_block_hash(
-                general_config=state.general_config,
-                parent_hash=int(parent_block_hash, 16),
-                block_number=block_number,
-                global_state_root=state_root,
-                block_timestamp=timestamp,
-                tx_hashes=[int(tx_wrapper.transaction_hash, 16)],
-                tx_signatures=[signature],
-                event_hashes=[],
-                sequencer_address=sequencer_address
-            )
-            self.__last_state_update["block_hash"] = hex(block_hash)
-            self.__hash2state_update[block_hash] = self.__last_state_update
-
-        block_hash_hexed = fixed_length_hex(block_hash)
-        block = {
-            "block_hash": block_hash_hexed,
-            "block_number": block_number,
-            "gas_price": hex(gas_price),
-            "parent_block_hash": parent_block_hash,
-            "sequencer_address": hex(sequencer_address),
-            "state_root": state_root.hex(),
-            "status": TxStatus.ACCEPTED_ON_L2.name,
-            "timestamp": timestamp,
-            "transaction_receipts": [tx_wrapper.get_receipt_block_variant()],
-            "transactions": [tx_wrapper.transaction["transaction"]],
-        }
-
-        self.__num2block[block_number] = block
-        self.__hash2block[block_hash] = block
-
-        return block_hash_hexed, block_number
-
-    def __get_last_block(self):
-        number_of_blocks = self.get_number_of_blocks()
-        return self.get_block_by_number(number_of_blocks - 1)
-
-    def get_block_by_hash(self, block_hash: str):
-        """Returns the block identified either by its `block_hash`"""
-
-        block_hash_int = int(block_hash, 16)
-        if block_hash_int in self.__hash2block:
-            return self.__hash2block[block_hash_int]
-        return self.origin.get_block_by_hash(block_hash=block_hash)
-
-    def get_block_by_number(self, block_number: int):
-        """Returns the block whose block_number is provided"""
-        if block_number is None:
-            if self.__num2block:
-                return self.__get_last_block()
-            return self.origin.get_block_by_number(block_number)
-
-        if block_number < 0:
-            message = f"Block number must be a non-negative integer; got: {block_number}."
-            raise StarknetDevnetException(message=message)
-
-        if block_number >= self.get_number_of_blocks():
-            message = f"Block number too high. There are currently {len(self.__num2block)} blocks; got: {block_number}."
-            raise StarknetDevnetException(message=message)
-
-        if block_number in self.__num2block:
-            return self.__num2block[block_number]
-
-        return self.origin.get_block_by_number(block_number)
-
-    # pylint: disable=too-many-arguments
-    async def __store_transaction(self, transaction: Transaction, contract_address: int, tx_hash: int, status: TxStatus,
-        execution_info: StarknetTransactionExecutionInfo, error_message: str=None, contract_hash: bytes=None
-    ):
-        """Stores the provided data as a deploy transaction in `self.transactions`."""
-        if transaction.tx_type == TransactionType.DEPLOY:
-            tx_wrapper = DeployTransactionWrapper(
-                transaction=transaction,
-                contract_address=contract_address,
-                tx_hash=tx_hash,
-                status=status,
-                execution_info=execution_info,
-                contract_hash=contract_hash,
-            )
-        elif transaction.tx_type == TransactionType.INVOKE_FUNCTION:
-            tx_wrapper = InvokeTransactionWrapper(transaction, status, execution_info)
-        else:
-            raise StarknetDevnetException(message=f"Illegal tx_type: {transaction.tx_type}")
-
-        if status == TxStatus.REJECTED:
-            assert error_message, "error_message must be present if tx rejected"
-            tx_wrapper.set_failure_reason(error_message)
-        else:
-            block_hash, block_number = await self.__generate_block(tx_wrapper)
-            tx_wrapper.set_block_data(block_hash, block_number)
-
-        numeric_hash = int(tx_wrapper.transaction_hash, 16)
-        self.__transaction_wrappers[numeric_hash] = tx_wrapper
-
-    def get_code(self, contract_address: int) -> dict:
-        """Returns a `dict` with `abi` and `bytecode` of the contract at `contract_address`."""
-        if self.__is_contract_deployed(contract_address):
-            contract_wrapper = self.__get_contract_wrapper(contract_address)
-            return contract_wrapper.code
-        return self.origin.get_code(contract_address)
-
-    def get_full_contract(self, contract_address: int) -> dict:
-        """Returns a `dict` contract definition of the contract at `contract_address`."""
-        contract_wrapper = self.__get_contract_wrapper(contract_address)
-        return contract_wrapper.contract_definition
-
     async def get_storage_at(self, contract_address: int, key: int) -> str:
         """
         Returns the storage identified by `key`
@@ -456,104 +278,15 @@ async def get_storage_at(self, contract_address: int, key: int) -> str:
         return self.origin.get_storage_at(contract_address, key)
 
     async def load_messaging_contract_in_l1(self, network_url: str, contract_address: str, network_id: str) -> dict:
-        """Creates a Postman Wrapper instance and loads an already deployed Messaging contract in the L1 network"""
-
-        # If no L1 network ID provided, will use a local testnet instance
-        if network_id is None or network_id == "local":
-            try:
-                starknet = await self.get_starknet()
-                starknet.state.l2_to_l1_messages_log.clear()
-                self.__postman_wrapper = LocalPostmanWrapper(network_url)
-                self.__postman_wrapper.load_mock_messaging_contract_in_l1(starknet,contract_address)
-            except Exception as error:
-                message = f"""Exception when trying to load the Starknet Messaging contract in a local testnet instance.
-Make sure you have a local testnet instance running at the provided network url, and that the Messaging Contract is deployed at the provided address
-Exception:
-{error}"""
-                raise StarknetDevnetException(message=message) from error
-        else:
-            message = "L1 interaction is only usable with a local running local testnet instance."
-            raise StarknetDevnetException(message=message)
-
-        self.__l1_provider = network_url
-        return {
-            "l1_provider": network_url,
-            "address": self.__postman_wrapper.mock_starknet_messaging_contract.address
-        }
+        """Loads the messaging contract at `contract_address`"""
+        starknet = await self.__get_starknet()
+        return self.l1l2.load_l1_messaging_contract(starknet, network_url, contract_address, network_id)
 
     async def postman_flush(self) -> dict:
         """Handles all pending L1 <> L2 messages and sends them to the other layer. """
 
         state = await self.__get_state()
-
-        if self.__postman_wrapper is None:
-            return {}
-
-        postman = self.__postman_wrapper.postman
-
-        l1_to_l2_messages = json.loads(Web3.toJSON(self.__postman_wrapper.l1_to_l2_message_filter.get_new_entries()))
-        l2_to_l1_messages = state.l2_to_l1_messages_log[postman.n_consumed_l2_to_l1_messages :]
-
-        await self.__postman_wrapper.flush()
-
-        return self.parse_l1_l2_messages(l1_to_l2_messages, l2_to_l1_messages)
-
-    def parse_l1_l2_messages(self, l1_raw_messages, l2_raw_messages) -> dict:
-        """Converts some of the values in the dictionaries from integer to hex and keys to snake_case."""
-
-        for message in l1_raw_messages:
-            message["args"]["selector"] = hex(message["args"]["selector"])
-            message["args"]["to_address"] = fixed_length_hex(message["args"].pop("toAddress")) # L2 addresses need the leading 0
-            message["args"]["from_address"] = message["args"].pop("fromAddress")
-            message["args"]["payload"] = [hex(val) for val in message["args"]["payload"]]
-
-            # change case to snake_case
-            message["transaction_hash"] = message.pop("transactionHash")
-            message["block_hash"] = message.pop("blockHash")
-            message["block_number"] = message.pop("blockNumber")
-            message["transaction_index"] = message.pop("transactionIndex")
-            message["log_index"] = message.pop("logIndex")
-
-        l2_messages = []
-        for message in l2_raw_messages:
-            new_message = {
-                "from_address": fixed_length_hex(message.from_address), # L2 addresses need the leading 0
-                "payload": [hex(val) for val in message.payload],
-                "to_address": hex(message.to_address)
-            }
-            l2_messages.append(new_message)
-
-        return {
-            "l1_provider": self.__l1_provider,
-            "consumed_messages": {
-                "from_l1": l1_raw_messages,
-                "from_l2": l2_messages
-            }
-        }
-
-    def get_state_update(self, block_hash=None, block_number=None):
-        """
-        Returns state update for the provided block hash or block number.
-        It will return the last state update if block is not provided.
-        """
-        if block_hash:
-            numeric_hash = int(block_hash, 16)
-
-            if numeric_hash in self.__hash2block:
-                return self.__hash2state_update[numeric_hash]
-
-            return self.origin.get_state_update(block_hash=block_hash)
-
-        if block_number is not None:
-            if block_number in self.__num2block:
-                block = self.__num2block[block_number]
-                numeric_hash = int(block["block_hash"], 16)
-
-                return self.__hash2state_update[numeric_hash]
-
-            return self.origin.get_state_update(block_number=block_number)
-
-        return self.__last_state_update or self.origin.get_state_update()
+        return await self.l1l2.flush(state)
 
     async def calculate_actual_fee(self, external_tx: InvokeFunction):
         """Calculates actual fee"""
@@ -570,3 +303,11 @@ async def calculate_actual_fee(self, external_tx: InvokeFunction):
         )
 
         return actual_fee
+
+    def increase_block_time(self, time_s: int):
+        """Increases the block time by `time_s`."""
+        self.block_info_generator.increase_time(time_s)
+
+    def set_block_time(self, time_s: int):
+        """Sets the block time to `time_s`."""
+        self.block_info_generator.set_next_block_time(time_s)
diff --git a/starknet_devnet/state.py b/starknet_devnet/state.py
index 084e0d929..8459ae498 100644
--- a/starknet_devnet/state.py
+++ b/starknet_devnet/state.py
@@ -2,7 +2,7 @@
 Global state singletone
 """
 
-from .starknet_wrapper import StarknetWrapper
+from .starknet_wrapper import StarknetWrapper, DevnetConfig
 from .dump import Dumper
 
 class State():
@@ -10,7 +10,7 @@ class State():
     Stores starknet wrapper and dumper
     """
     def __init__(self):
-        self.starknet_wrapper = StarknetWrapper()
+        self.starknet_wrapper = StarknetWrapper(config=DevnetConfig())
         self.dumper = Dumper(self.starknet_wrapper)
 
     def __set_starknet_wrapper(self, starknet_wrapper: StarknetWrapper):
@@ -18,9 +18,10 @@ def __set_starknet_wrapper(self, starknet_wrapper: StarknetWrapper):
         self.starknet_wrapper = starknet_wrapper
         self.dumper = Dumper(starknet_wrapper)
 
-    def reset(self):
+    def reset(self, config: DevnetConfig = None):
         """Reset the starknet wrapper and dumper instances"""
-        self.__set_starknet_wrapper(StarknetWrapper())
+        previous_config = self.starknet_wrapper.config
+        self.__set_starknet_wrapper(StarknetWrapper(config=config or previous_config))
 
     def load(self, load_path: str):
         """Loads starknet wrapper from path"""
diff --git a/starknet_devnet/transactions.py b/starknet_devnet/transactions.py
new file mode 100644
index 000000000..4d413f028
--- /dev/null
+++ b/starknet_devnet/transactions.py
@@ -0,0 +1,99 @@
+"""
+Class for storing and handling transactions.
+"""
+
+from .origin import Origin
+from .transaction_wrapper import TransactionWrapper
+from .constants import FAILURE_REASON_KEY
+
+
+class DevnetTransactions:
+    """
+    This class is used to store transactions.
+    """
+
+    def __init__(self, origin: Origin):
+        self.origin = origin
+        self.__instances = {}
+
+    def __get_transaction_by_hash(self, tx_hash: str) -> TransactionWrapper or None:
+        """
+        Get a transaction by hash.
+        """
+        numeric_hash = int(tx_hash, 16)
+        return self.__instances.get(numeric_hash)
+
+    def get_count(self):
+        """
+        Get the number of transactions.
+        """
+        return len(self.__instances)
+
+    def store(self, transaction: TransactionWrapper):
+        """
+        Store a transaction.
+        """
+        numeric_hash = int(transaction.transaction_hash, 16)
+        self.__instances[numeric_hash] = transaction
+
+    def get_transaction(self, tx_hash: str):
+        """
+        Get a transaction.
+        """
+        transaction = self.__get_transaction_by_hash(tx_hash)
+
+        if transaction is None:
+            return self.origin.get_transaction(tx_hash)
+
+        return transaction.transaction
+
+    def get_transaction_trace(self, tx_hash: str):
+        """
+        Get a transaction trace.
+        """
+        transaction = self.__get_transaction_by_hash(tx_hash)
+
+        if transaction is None:
+            return self.origin.get_transaction_trace(tx_hash)
+
+        return transaction.trace
+
+    def get_transaction_receipt(self, tx_hash: str):
+        """
+        Get a transaction receipt.
+        """
+        transaction = self.__get_transaction_by_hash(tx_hash)
+
+        if transaction is None:
+            return self.origin.get_transaction_receipt(tx_hash)
+
+        return transaction.receipt
+
+    def get_transaction_status(self, tx_hash: str):
+        """
+        Get a transaction status.
+        """
+        transaction_wrapper = self.__get_transaction_by_hash(tx_hash)
+
+        if transaction_wrapper is None:
+            return self.origin.get_transaction_status(tx_hash)
+
+        transaction = transaction_wrapper.transaction
+
+        # the transaction status object only needs 1-3 elements from the transaction_wrapper object
+        status_response = {
+            # "tx_status" always exists
+            "tx_status": transaction["status"]
+        }
+
+        # "block_hash" will only exist after transaction enters ACCEPTED_ON_L2
+        if "block_hash" in transaction:
+            status_response["block_hash"] = transaction["block_hash"]
+
+        # "tx_failure_reason" will only exist if the transaction was rejected.
+        # the key in the transaction_wrapper object is "transaction_failure_reason"
+        # first it must be checked if the object contains an element with that key
+        if FAILURE_REASON_KEY in transaction:
+            status_response["tx_failure_reason"] = transaction[FAILURE_REASON_KEY]
+
+        return status_response
diff --git a/starknet_devnet/util.py b/starknet_devnet/util.py
index ee6ea73b3..e146646fe 100644
--- a/starknet_devnet/util.py
+++ b/starknet_devnet/util.py
@@ -13,6 +13,9 @@
 
 from . import __version__
 
+DEFAULT_HOST = "127.0.0.1"
+DEFAULT_PORT = 5050
+
 class TxStatus(Enum):
     """
     According to: https://www.cairo-lang.org/docs/hello_starknet/intro.html#interact-with-the-contract
@@ -78,8 +81,17 @@ def parse_dump_on(option: str):
         return DumpOn[option.upper()]
     sys.exit(f"Error: Invalid --dump-on option: {option}. Valid options: {DUMP_ON_OPTIONS_STRINGIFIED}")
 
-DEFAULT_HOST = "127.0.0.1"
-DEFAULT_PORT = 5050
+class NonNegativeAction(argparse.Action):
+    """
+    Action for parsing the non negative int argument.
+    """
+    def __call__(self, parser, namespace, values, option_string=None):
+        value = int(values)
+
+        if value < 0:
+            parser.error(f"{option_string} must be a positive integer.")
+
+        setattr(namespace, self.dest, value)
 
 def parse_args():
     """
@@ -132,6 +144,11 @@ def parse_args():
         action='store_true',
         help="Disables deploy tx hash calculation"
     )
+    parser.add_argument(
+        "--start-time",
+        action=NonNegativeAction,
+        help="Specify the start time of the genesis block in Unix time"
+    )
     # Uncomment this once fork support is added
     # parser.add_argument(
     #     "--fork", "-f",


Test patch:
diff --git a/test/test_timestamps.py b/test/test_timestamps.py
index c55a53fdf..0beeebe03 100644
--- a/test/test_timestamps.py
+++ b/test/test_timestamps.py
@@ -2,14 +2,22 @@
 Test block timestamps
 """
 
+import math
+import time
 import pytest
+import requests
+
+from starknet_devnet.block_info_generator import BlockInfoGenerator, BlockInfo
+from starknet_devnet.general_config import DEFAULT_GENERAL_CONFIG
 
 from .shared import ARTIFACTS_PATH
 from .util import devnet_in_background, deploy, call, get_block
+from .settings import APP_URL
 
 TS_CONTRACT_PATH = f"{ARTIFACTS_PATH}/timestamp.cairo/timestamp.json"
 TS_ABI_PATH = f"{ARTIFACTS_PATH}/timestamp.cairo/timestamp_abi.json"
 
+SET_TIME_ARGUMENT = 1514764800
 
 def deploy_ts_contract():
     """Deploys the timestamp contract"""
@@ -27,6 +35,24 @@ def get_ts_from_last_block():
     """Returns the timestamp of the last block"""
     return get_block(parse=True)["timestamp"]
 
+def increase_time(time_s):
+    """Increases the block timestamp offset"""
+    increase_time_response = requests.post(f"{APP_URL}/increase_time", json={"time": time_s})
+
+    if increase_time_response.status_code == 200:
+        assert increase_time_response.json().get("timestamp_increased_by") == time_s
+
+    return increase_time_response
+
+def set_time(time_s):
+    """Sets the block timestamp and offset"""
+    set_time_response = requests.post(f"{APP_URL}/set_time", json={"time": time_s})
+
+    if set_time_response == 200:
+        assert set_time_response.json().get("next_block_timestamp") == time_s
+
+    return set_time_response
+
 @pytest.mark.timestamps
 @devnet_in_background()
 def test_timestamps():
@@ -38,7 +64,7 @@ def test_timestamps():
 
     assert ts_after_deploy == ts_from_first_call
 
-    # deploy another contract contract to generate a new block
+    # deploy another contract to generate a new block
     deploy_ts_contract()
     ts_after_second_deploy = get_ts_from_last_block()
 
@@ -48,3 +74,156 @@ def test_timestamps():
 
     assert ts_after_second_deploy == ts_from_second_call
     assert ts_from_second_call > ts_from_first_call
+
+@pytest.mark.timestamps
+@devnet_in_background()
+def test_increase_time():
+    """Test timestamp increase time"""
+    start = time.time()
+    deploy_info = deploy_ts_contract()
+    ts_after_deploy = get_ts_from_last_block()
+
+    first_block_ts = get_ts_from_contract(deploy_info["address"])
+
+    assert ts_after_deploy == first_block_ts
+
+    # increase time by 1 day
+    increase_time(86400)
+
+    # deploy another contract to generate a new block
+    deploy_ts_contract()
+
+    second_block_ts = get_ts_from_last_block()
+
+    assert second_block_ts - first_block_ts >= 86400
+    elapsed_time = math.ceil(time.time() - start)
+    assert second_block_ts < first_block_ts + 86400 + elapsed_time
+
+
+@pytest.mark.timestamps
+@devnet_in_background()
+def test_set_time():
+    """Test timestamp set time"""
+    deploy_info = deploy_ts_contract()
+    first_block_ts = get_ts_from_last_block()
+
+    ts_from_first_call = get_ts_from_contract(deploy_info["address"])
+
+    assert first_block_ts == ts_from_first_call
+
+    # set time to 1 day after the deploy
+    set_time(first_block_ts + 86400)
+
+    ts_after_set = get_ts_from_last_block()
+
+    assert ts_after_set == first_block_ts
+
+    # generate a new block by deploying a new contract
+    deploy_ts_contract()
+
+    second_block_ts = get_ts_from_last_block()
+
+    assert second_block_ts == first_block_ts + 86400
+
+    # generate a new block by deploying a new contract
+    deploy_ts_contract()
+
+    third_block_ts = get_ts_from_last_block()
+
+    # check if offset is still the same
+    assert third_block_ts - first_block_ts >= 86400
+
+@pytest.mark.timestamps
+@devnet_in_background("--start-time", str(SET_TIME_ARGUMENT))
+def test_set_time_argument():
+    """Test timestamp set time argument"""
+    deploy_ts_contract()
+    first_block_ts = get_ts_from_last_block()
+
+    assert first_block_ts == SET_TIME_ARGUMENT
+
+@pytest.mark.timestamps
+@devnet_in_background()
+def test_set_time_errors():
+    """Test timestamp set time negative"""
+    deploy_ts_contract()
+
+    response = set_time(-1)
+    message = response.json()["message"]
+
+    assert response.status_code == 400
+    assert message == "Time value must be greater than 0."
+
+    response = set_time(None)
+    message = response.json()["message"]
+
+    assert response.status_code == 400
+    assert message == "Time value must be provided."
+
+    response = set_time("not an int")
+    message = response.json()["message"]
+
+    assert response.status_code == 400
+    assert message == "Time value must be an integer."
+
+@pytest.mark.timestamps
+@devnet_in_background()
+def test_increase_time_errors():
+    """Test timestamp increase time negative"""
+    deploy_ts_contract()
+
+    response = increase_time(-1)
+    message = response.json()["message"]
+
+    assert response.status_code == 400
+    assert message == "Time value must be greater than 0."
+
+    response = increase_time(None)
+    message = response.json()["message"]
+
+    assert response.status_code == 400
+    assert message == "Time value must be provided."
+
+    response = increase_time("not an int")
+    message = response.json()["message"]
+
+    assert response.status_code == 400
+    assert message == "Time value must be an integer."
+
+
+@pytest.mark.timestamps
+def test_block_info_generator():
+    """Test block info generator"""
+    start = int(time.time())
+    block_info = BlockInfo.create_for_testing(block_number=0, block_timestamp=start)
+
+
+    # Test if start time is set by the constructor
+    generator = BlockInfoGenerator(start_time=10)
+
+    block_with_start_time = generator.next_block(block_info=block_info, general_config=DEFAULT_GENERAL_CONFIG)
+
+    assert block_with_start_time.block_timestamp == 10
+
+    # Check if set time can be incrased
+
+    generator.increase_time(22)
+
+    block_after_increase = generator.next_block(block_info=block_info, general_config=DEFAULT_GENERAL_CONFIG)
+
+    assert block_after_increase.block_timestamp == 32
+
+    # Test if start time can be set after increase
+
+    generator = BlockInfoGenerator()
+    generator.increase_time(1_000_000_000)
+
+    block_with_increase_time = generator.next_block(block_info=block_info, general_config=DEFAULT_GENERAL_CONFIG)
+
+    assert block_with_increase_time.block_timestamp >= 1_000_000_000 + int(time.time())
+
+
+    generator.set_next_block_time(222)
+    block_after_set_time = generator.next_block(block_info=block_info, general_config=DEFAULT_GENERAL_CONFIG)
+
+    assert block_after_set_time.block_timestamp == 222