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

Issue:
Support message fee estimation
Starknet v0.10 ([release notes](https://github.com/starkware-libs/cairo-lang/releases/tag/v0.10.0)) introduced a new method to the sequencer API: `/feeder_gateway/estimate_message_fee`
It is targeted by `starknet estimate_message_fee`:
- [Client implementation](https://github.com/starkware-libs/cairo-lang/blob/v0.10.0/src/starkware/starknet/services/api/feeder_gateway/feeder_gateway_client.py#L68)
- [Usage example](https://www.starknet.io/docs/0.10.0/hello_starknet/l1l2.html#estimate-message-fee)


Discussion / hints:

Fix patch:
diff --git a/README.md b/README.md
index ca33552de..3f66c8669 100644
--- a/README.md
+++ b/README.md
@@ -178,6 +178,7 @@ If you don't specify the `HOST` part, the server will indeed be available on all
   - `declare`
   - `deploy`
   - `estimate_fee`
+  - `estimate_message_fee`
   - `get_block` (currently pending block is not supported)
   - `get_block_traces`
   - `get_class_by_hash`
diff --git a/starknet_devnet/blueprints/feeder_gateway.py b/starknet_devnet/blueprints/feeder_gateway.py
index 911025608..7b94dc8fa 100644
--- a/starknet_devnet/blueprints/feeder_gateway.py
+++ b/starknet_devnet/blueprints/feeder_gateway.py
@@ -12,10 +12,12 @@
     InvokeFunction,
 )
 from starkware.starknet.services.api.gateway.transaction import Transaction
-from starkware.starknet.services.api.feeder_gateway.request_objects import CallFunction
+from starkware.starknet.services.api.feeder_gateway.request_objects import (
+    CallFunction,
+    CallL1Handler,
+)
 from starkware.starknet.services.api.feeder_gateway.response_objects import (
     TransactionSimulationInfo,
-    FeeEstimationInfo,
 )
 from werkzeug.datastructures import MultiDict
 
@@ -276,7 +278,7 @@ async def estimate_fee():
         transaction = validate_request(request.data, InvokeFunction)  # version 0
 
     _, fee_response = await state.starknet_wrapper.calculate_trace_and_fee(transaction)
-    return jsonify(FeeEstimationInfo.load(fee_response))
+    return jsonify(fee_response)
 
 
 @feeder_gateway.route("/simulate_transaction", methods=["POST"])
@@ -288,7 +290,7 @@ async def simulate_transaction():
     )
 
     simulation_info = TransactionSimulationInfo(
-        trace=trace, fee_estimation=FeeEstimationInfo.load(fee_response)
+        trace=trace, fee_estimation=fee_response
     )
 
     return jsonify(simulation_info.dump())
@@ -302,3 +304,14 @@ async def get_nonce():
     nonce = await state.starknet_wrapper.get_nonce(contract_address)
 
     return jsonify(hex(nonce))
+
+
+@feeder_gateway.route("/estimate_message_fee", methods=["POST"])
+async def estimate_message_fee():
+    """Message fee estimation endpoint"""
+
+    _check_block_hash(request.args)
+
+    call = validate_request(request.data, CallL1Handler)
+    fee_estimation = await state.starknet_wrapper.estimate_message_fee(call)
+    return jsonify(fee_estimation)
diff --git a/starknet_devnet/blueprints/rpc/structures/payloads.py b/starknet_devnet/blueprints/rpc/structures/payloads.py
index c9ec20522..b6152bd45 100644
--- a/starknet_devnet/blueprints/rpc/structures/payloads.py
+++ b/starknet_devnet/blueprints/rpc/structures/payloads.py
@@ -16,6 +16,7 @@
     TransactionType,
     BlockStateUpdate,
     DeclareSpecificInfo,
+    FeeEstimationInfo,
 )
 from starkware.starknet.services.api.gateway.transaction import InvokeFunction
 from starkware.starknet.services.api.gateway.transaction_utils import compress_program
@@ -231,14 +232,14 @@ class RpcFeeEstimate(TypedDict):
     overall_fee: NumAsHex
 
 
-def rpc_fee_estimate(fee_estimate: dict) -> dict:
+def rpc_fee_estimate(fee_estimate: FeeEstimationInfo) -> dict:
     """
     Convert gateway estimate_fee response to rpc_fee_estimate
     """
     result: RpcFeeEstimate = {
-        "gas_consumed": hex(fee_estimate["gas_usage"]),
-        "gas_price": hex(fee_estimate["gas_price"]),
-        "overall_fee": hex(fee_estimate["overall_fee"]),
+        "gas_consumed": hex(fee_estimate.gas_usage),
+        "gas_price": hex(fee_estimate.gas_price),
+        "overall_fee": hex(fee_estimate.overall_fee),
     }
     return result
 
diff --git a/starknet_devnet/starknet_wrapper.py b/starknet_devnet/starknet_wrapper.py
index 84e5c3a69..7a9bce2d6 100644
--- a/starknet_devnet/starknet_wrapper.py
+++ b/starknet_devnet/starknet_wrapper.py
@@ -6,11 +6,13 @@
 from typing import Dict, List, Set, Tuple, Union
 
 import cloudpickle as pickle
+from starkware.starknet.business_logic.transaction.fee import calculate_tx_fee
 from starkware.starknet.business_logic.transaction.objects import (
     CallInfo,
     InternalInvokeFunction,
     InternalDeclare,
     InternalDeploy,
+    InternalL1Handler,
 )
 from starkware.starknet.business_logic.state.state import BlockInfo, CachedState
 from starkware.starknet.services.api.gateway.transaction import (
@@ -21,10 +23,14 @@
 from starkware.starknet.testing.starknet import Starknet
 from starkware.starkware_utils.error_handling import StarkException
 from starkware.starknet.services.api.contract_class import EntryPointType, ContractClass
-from starkware.starknet.services.api.feeder_gateway.request_objects import CallFunction
+from starkware.starknet.services.api.feeder_gateway.request_objects import (
+    CallFunction,
+    CallL1Handler,
+)
 from starkware.starknet.services.api.feeder_gateway.response_objects import (
     TransactionStatus,
 )
+
 from starkware.starknet.testing.contract import StarknetContract
 from starkware.starknet.testing.objects import FunctionInvocation
 from starkware.starknet.services.api.feeder_gateway.response_objects import (
@@ -37,7 +43,7 @@
     StorageEntry,
 )
 
-from starknet_devnet.util import to_bytes
+from starknet_devnet.util import to_bytes, get_fee_estimation_info
 from starknet_devnet.constants import DUMMY_STATE_ROOT
 
 from .lite_mode.lite_internal_deploy import LiteInternalDeploy
@@ -519,17 +525,35 @@ async def calculate_trace_and_fee(self, external_tx: InvokeFunction):
             signature=external_tx.signature,
         )
 
-        tx_fee = execution_info.actual_fee
+        fee_estimation_info = get_fee_estimation_info(
+            execution_info.actual_fee, state.state.block_info.gas_price
+        )
+
+        return trace, fee_estimation_info
+
+    async def estimate_message_fee(self, call: CallL1Handler):
+        """Estimate fee of message from L1 to L2"""
+        state = self.get_state()
+        internal_call: InternalL1Handler = call.to_internal(
+            state.general_config.chain_id.value
+        )
+
+        execution_info = await internal_call.apply_state_updates(
+            # pylint: disable=protected-access
+            state.state._copy(),
+            state.general_config,
+        )
 
-        gas_price = state.state.block_info.gas_price
-        gas_usage = tx_fee // gas_price if gas_price else 0
+        actual_fee = calculate_tx_fee(
+            resources=execution_info.actual_resources,
+            gas_price=state.general_config.min_gas_price,
+            general_config=state.general_config,
+        )
 
-        return trace, {
-            "overall_fee": tx_fee,
-            "unit": "wei",
-            "gas_price": gas_price,
-            "gas_usage": gas_usage,
-        }
+        fee_estimation_info = get_fee_estimation_info(
+            actual_fee, state.state.block_info.gas_price
+        )
+        return fee_estimation_info
 
     def increase_block_time(self, time_s: int):
         """Increases the block time by `time_s`."""
diff --git a/starknet_devnet/util.py b/starknet_devnet/util.py
index 4a31311d5..e3aceab7f 100644
--- a/starknet_devnet/util.py
+++ b/starknet_devnet/util.py
@@ -12,6 +12,7 @@
 from starkware.starknet.business_logic.state.state import CachedState
 from starkware.starknet.services.api.feeder_gateway.response_objects import (
     DeployedContract,
+    FeeEstimationInfo,
     StorageEntry,
 )
 
@@ -162,3 +163,18 @@ async def get_storage_diffs(
             )
 
     return storage_diffs
+
+
+def get_fee_estimation_info(tx_fee: int, gas_price: int):
+    """Construct fee estimation response"""
+
+    gas_usage = tx_fee // gas_price if gas_price else 0
+
+    return FeeEstimationInfo.load(
+        {
+            "overall_fee": tx_fee,
+            "unit": "wei",
+            "gas_price": gas_price,
+            "gas_usage": gas_usage,
+        }
+    )


Test patch:
diff --git a/test/contracts/cairo/l1l2.cairo b/test/contracts/cairo/l1l2.cairo
index e8c900670..1a072e6a3 100644
--- a/test/contracts/cairo/l1l2.cairo
+++ b/test/contracts/cairo/l1l2.cairo
@@ -59,7 +59,7 @@ func withdraw{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}(
 func deposit{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}(
     from_address: felt, user: felt, amount: felt
 ) {
-    // Make sure the message was sent by the intended L1 contract.
+    // In a real case scenario, here we would assert from_address value
 
     // Read the current balance.
     let (res) = balance.read(user=user);
diff --git a/test/rpc/test_rpc_transactions.py b/test/rpc/test_rpc_transactions.py
index 6ba6b0c73..36763d445 100644
--- a/test/rpc/test_rpc_transactions.py
+++ b/test/rpc/test_rpc_transactions.py
@@ -313,6 +313,7 @@ def test_add_invoke_transaction(invoke_content):
     assert set(receipt.keys()) == {"transaction_hash"}
     assert receipt["transaction_hash"][:3] == "0x0"
 
+
 @pytest.mark.usefixtures("run_devnet_in_background")
 def test_add_invoke_transaction_positional_args(invoke_content):
     """
@@ -340,6 +341,7 @@ def test_add_invoke_transaction_positional_args(invoke_content):
     assert set(receipt.keys()) == {"transaction_hash"}
     assert receipt["transaction_hash"][:3] == "0x0"
 
+
 @pytest.mark.usefixtures("run_devnet_in_background")
 def test_add_declare_transaction_on_incorrect_contract(declare_content):
     """
diff --git a/test/shared.py b/test/shared.py
index 9f0d34e4c..6bda456c6 100644
--- a/test/shared.py
+++ b/test/shared.py
@@ -11,6 +11,9 @@
 DEPLOYER_CONTRACT_PATH = f"{ARTIFACTS_PATH}/deployer.cairo/deployer.json"
 DEPLOYER_ABI_PATH = f"{ARTIFACTS_PATH}/deployer.cairo/deployer_abi.json"
 
+L1L2_CONTRACT_PATH = f"{ARTIFACTS_PATH}/l1l2.cairo/l1l2.json"
+L1L2_ABI_PATH = f"{ARTIFACTS_PATH}/l1l2.cairo/l1l2_abi.json"
+
 BALANCE_KEY = (
     "916907772491729262376534102982219947830828984996257231353398618781993312401"
 )
diff --git a/test/test_estimate_fee.py b/test/test_estimate_fee.py
index 57b40d960..7c4ec250c 100644
--- a/test/test_estimate_fee.py
+++ b/test/test_estimate_fee.py
@@ -8,9 +8,21 @@
 
 
 from starknet_devnet.constants import DEFAULT_GAS_PRICE
-from .util import deploy, devnet_in_background, load_file_content
+from .util import (
+    call,
+    deploy,
+    devnet_in_background,
+    estimate_message_fee,
+    load_file_content,
+)
 from .settings import APP_URL
-from .shared import CONTRACT_PATH, EXPECTED_CLASS_HASH
+from .shared import (
+    CONTRACT_PATH,
+    EXPECTED_CLASS_HASH,
+    L1L2_ABI_PATH,
+    L1L2_CONTRACT_PATH,
+    PREDEPLOY_ACCOUNT_CLI_ARGS,
+)
 
 DEPLOY_CONTENT = load_file_content("deploy.json")
 INVOKE_CONTENT = load_file_content("invoke.json")
@@ -165,3 +177,30 @@ def test_simulate_transaction():
         },
         "signature": [],
     }
+
+
+@devnet_in_background(*PREDEPLOY_ACCOUNT_CLI_ARGS)
+def test_estimate_message_fee():
+    """Estimate message fee from l1 to l2"""
+
+    dummy_l1_address = "0x1"
+    user_id = "1"
+
+    l2_contract_address = deploy(contract=L1L2_CONTRACT_PATH)["address"]
+
+    message_fee = estimate_message_fee(
+        from_address=dummy_l1_address,
+        function="deposit",
+        inputs=[user_id, "100"],
+        to_address=l2_contract_address,
+        abi_path=L1L2_ABI_PATH,
+    )
+    assert int(message_fee) > 0
+
+    balance_after = call(
+        function="get_balance",
+        address=l2_contract_address,
+        abi_path=L1L2_ABI_PATH,
+        inputs=[user_id],
+    )
+    assert int(balance_after) == 0
diff --git a/test/test_postman.py b/test/test_postman.py
index 783b9a599..c4bdf7a8f 100644
--- a/test/test_postman.py
+++ b/test/test_postman.py
@@ -17,27 +17,28 @@
     deploy,
     devnet_in_background,
     ensure_server_alive,
+    estimate_message_fee,
     load_file_content,
     terminate_and_wait,
 )
 from .settings import APP_URL, L1_HOST, L1_PORT, L1_URL
 from .shared import (
-    ARTIFACTS_PATH,
+    L1L2_ABI_PATH,
+    L1L2_CONTRACT_PATH,
     PREDEPLOY_ACCOUNT_CLI_ARGS,
     PREDEPLOYED_ACCOUNT_ADDRESS,
     PREDEPLOYED_ACCOUNT_PRIVATE_KEY,
 )
 from .web3_util import web3_call, web3_deploy, web3_transact
 
-CONTRACT_PATH = f"{ARTIFACTS_PATH}/l1l2.cairo/l1l2.json"
-ABI_PATH = f"{ARTIFACTS_PATH}/l1l2.cairo/l1l2_abi.json"
-
 ETH_CONTRACTS_PATH = "artifacts/contracts/solidity"
 STARKNET_MESSAGING_PATH = (
     f"{ETH_CONTRACTS_PATH}/MockStarknetMessaging.sol/MockStarknetMessaging.json"
 )
 L1L2_EXAMPLE_PATH = f"{ETH_CONTRACTS_PATH}/L1L2.sol/L1L2Example.json"
 
+USER_ID = 1
+
 
 @pytest.fixture(autouse=True)
 def run_before_and_after_test():
@@ -159,22 +160,21 @@ def load_messaging_contract(starknet_messaging_contract_address):
     return json.loads(resp.text)
 
 
-def _init_l2_contract(l1l2_example_contract_address):
+def _init_l2_contract(l1l2_example_contract_address: str):
     """Deploys the L1L2Example cairo contract, returns the result of calling 'get_balance'"""
 
-    deploy_info = deploy(CONTRACT_PATH)
+    deploy_info = deploy(L1L2_CONTRACT_PATH)
     l2_address = deploy_info["address"]
 
     # increase and withdraw balance
     invoke(
-        calls=[(l2_address, "increase_balance", [1, 3333])],
+        calls=[(l2_address, "increase_balance", [USER_ID, 3333])],
         account_address=PREDEPLOYED_ACCOUNT_ADDRESS,
         private_key=PREDEPLOYED_ACCOUNT_PRIVATE_KEY,
     )
+    contract_address_int = int(l1l2_example_contract_address, 16)
     invoke(
-        calls=[
-            (l2_address, "withdraw", [1, 1000, int(l1l2_example_contract_address, 16)])
-        ],
+        calls=[(l2_address, "withdraw", [USER_ID, 1000, contract_address_int])],
         account_address=PREDEPLOYED_ACCOUNT_ADDRESS,
         private_key=PREDEPLOYED_ACCOUNT_PRIVATE_KEY,
     )
@@ -189,7 +189,7 @@ def _init_l2_contract(l1l2_example_contract_address):
             {
                 "from_address": deploy_info["address"],
                 "to_address": l1l2_example_contract_address,
-                "payload": [0, 1, 1000],  # MESSAGE_WITHDRAW, user, amount
+                "payload": [0, USER_ID, 1000],  # MESSAGE_WITHDRAW, user, amount
             }
         ],
         expected_l1_provider=L1_URL,
@@ -199,8 +199,8 @@ def _init_l2_contract(l1l2_example_contract_address):
     value = call(
         function="get_balance",
         address=deploy_info["address"],
-        abi_path=ABI_PATH,
-        inputs=["1"],
+        abi_path=L1L2_ABI_PATH,
+        inputs=[str(USER_ID)],
     )
 
     assert value == "2333"
@@ -211,7 +211,7 @@ def _l1_l2_message_exchange(web3, l1l2_example_contract, l2_contract_address):
     """Tests message exchange"""
 
     # assert contract balance when starting
-    balance = web3_call("userBalances", l1l2_example_contract, 1)
+    balance = web3_call("userBalances", l1l2_example_contract, USER_ID)
     assert balance == 0
 
     # withdraw in l1 and assert contract balance
@@ -220,34 +220,42 @@ def _l1_l2_message_exchange(web3, l1l2_example_contract, l2_contract_address):
         "withdraw",
         l1l2_example_contract,
         int(l2_contract_address, base=16),
-        1,
+        USER_ID,
         1000,
     )
 
-    balance = web3_call("userBalances", l1l2_example_contract, 1)
+    balance = web3_call("userBalances", l1l2_example_contract, USER_ID)
     assert balance == 1000
 
     # assert l2 contract balance
     l2_balance = call(
         function="get_balance",
         address=l2_contract_address,
-        abi_path=ABI_PATH,
-        inputs=["1"],
+        abi_path=L1L2_ABI_PATH,
+        inputs=[str(USER_ID)],
     )
-
     assert l2_balance == "2333"
 
+    message_fee = estimate_message_fee(
+        from_address=l1l2_example_contract.address,
+        function="deposit",
+        inputs=[str(USER_ID), "100"],
+        to_address=l2_contract_address,
+        abi_path=L1L2_ABI_PATH,
+    )
+    assert int(message_fee) > 0
+
     # deposit in l1 and assert contract balance
     web3_transact(
         web3,
         "deposit",
         l1l2_example_contract,
         int(l2_contract_address, base=16),
-        1,
+        USER_ID,
         600,
     )
 
-    balance = web3_call("userBalances", l1l2_example_contract, 1)
+    balance = web3_call("userBalances", l1l2_example_contract, USER_ID)
 
     assert balance == 400
 
@@ -262,7 +270,7 @@ def _l1_l2_message_exchange(web3, l1l2_example_contract, l2_contract_address):
                 "args": {
                     "from_address": l1l2_example_contract.address,
                     "to_address": l2_contract_address,
-                    "payload": [1, 600],  # user, amount
+                    "payload": [USER_ID, 600],  # user, amount
                 },
             }
         ],
@@ -274,8 +282,8 @@ def _l1_l2_message_exchange(web3, l1l2_example_contract, l2_contract_address):
     l2_balance = call(
         function="get_balance",
         address=l2_contract_address,
-        abi_path=ABI_PATH,
-        inputs=["1"],
+        abi_path=L1L2_ABI_PATH,
+        inputs=[str(USER_ID)],
     )
 
     assert l2_balance == "2933"
@@ -310,18 +318,19 @@ def test_postman():
     _l1_l2_message_exchange(web3, l1l2_example_contract, l2_contract_address)
 
 
-def load_l1_messaging_contract(req_dict: dict):
+def _load_l1_messaging_contract(req_dict: dict):
     """Load L1 messaging contract"""
     return requests.post(
         f"{APP_URL}/postman/load_l1_messaging_contract", json=(req_dict)
     )
 
 
+@pytest.mark.web3_messaging
 @devnet_in_background(*PREDEPLOY_ACCOUNT_CLI_ARGS)
 def test_invalid_starknet_function_call_load_l1_messaging_contract():
     """Call with invalid data on starknet function call"""
     load_messaging_contract_request = {}
-    resp = load_l1_messaging_contract(load_messaging_contract_request)
+    resp = _load_l1_messaging_contract(load_messaging_contract_request)
 
     json_error_message = resp.json()["message"]
     msg = "L1 network or StarknetMessaging contract address not specified"
diff --git a/test/util.py b/test/util.py
index d6d192140..487b3da71 100644
--- a/test/util.py
+++ b/test/util.py
@@ -8,6 +8,7 @@
 import re
 import subprocess
 import time
+from typing import List
 import requests
 
 from starkware.starknet.services.api.contract_class import ContractClass
@@ -134,7 +135,7 @@ def extract_tx_hash(stdout):
 
 def extract_fee(stdout) -> int:
     """Extract fee from stdout."""
-    return int(extract(r"(\d+)", stdout))
+    return int(extract(r"The estimated fee is: (\d+) WEI", stdout))
 
 
 def extract_address(stdout):
@@ -169,6 +170,29 @@ def deploy(contract, inputs=None, salt=None):
     }
 
 
+def estimate_message_fee(
+    from_address: str, function: str, inputs: List[str], to_address: str, abi_path: str
+):
+    """Wrapper around starknet estimate_message_fee"""
+    output = run_starknet(
+        [
+            "estimate_message_fee",
+            "--from_address",
+            from_address,
+            "--function",
+            function,
+            "--inputs",
+            *inputs,
+            "--address",
+            to_address,
+            "--abi",
+            abi_path,
+        ]
+    )
+
+    return extract_fee(output.stdout)
+
+
 def assert_transaction(tx_hash, expected_status, expected_signature=None):
     """Wrapper around starknet get_transaction"""
     output = run_starknet(["get_transaction", "--hash", tx_hash])