From ad5f644c1b848792409f148c5b8e5e0ceea5f905 Mon Sep 17 00:00:00 2001 From: Artur Michalek Date: Mon, 27 Jun 2022 13:43:56 +0200 Subject: [PATCH 1/9] Initial work on updating rpc to 0.15 --- starknet_devnet/blueprints/rpc.py | 95 +++++++++++++++++++++++++++++++ test/test_rpc_endpoints.py | 67 ++++++++++++++++++++++ 2 files changed, 162 insertions(+) diff --git a/starknet_devnet/blueprints/rpc.py b/starknet_devnet/blueprints/rpc.py index b734d8c0c..29767ea1f 100644 --- a/starknet_devnet/blueprints/rpc.py +++ b/starknet_devnet/blueprints/rpc.py @@ -4,8 +4,11 @@ """ from __future__ import annotations +import base64 import json from typing import Callable, Union, List, Tuple, Optional, Any + +from starkware.starknet.services.api.contract_class import ContractClass, EntryPointType from typing_extensions import TypedDict from flask import Blueprint, request @@ -181,6 +184,37 @@ async def get_code(contract_address: str) -> dict: } +async def get_class(class_hash: str) -> dict: + """ + Get the code of a specific contract + """ + try: + result = state.starknet_wrapper.contracts.get_class_by_hash(class_hash=int(class_hash, 16)) + except StarknetDevnetException as ex: + raise RpcError(code=28, message="The supplied contract class hash is invalid or unknown") from ex + + return rpc_contract_class(result) + + +async def get_class_hash_at(contract_address: str) -> str: + try: + result = state.starknet_wrapper.contracts.get_class_hash_at(address=int(contract_address, 16)) + except StarknetDevnetException as ex: + raise RpcError(code=28, message="The supplied contract class hash is invalid or unknown") from ex + + return rpc_felt(result) + + +async def get_class_at(contract_address: str) -> dict: + try: + class_hash = state.starknet_wrapper.contracts.get_class_hash_at(address=int(contract_address, 16)) + result = state.starknet_wrapper.contracts.get_class_by_hash(class_hash=class_hash) + except StarknetDevnetException as ex: + raise RpcError(code=20, message="Contract not found") from ex + + return rpc_contract_class(result) + + async def get_block_transaction_count_by_hash(block_hash: str) -> int: """ Get the number of transactions in a block given a block hash @@ -235,6 +269,10 @@ async def call(contract_address: str, entry_point_selector: str, calldata: list, raise RpcError(code=-1, message=ex.message) from ex +async def estimate_fee(): + pass + + async def get_block_number() -> int: """ Get the most recent accepted block number @@ -296,6 +334,59 @@ def make_invoke_function(request_body: dict) -> InvokeFunction: ) +class EntryPoint(TypedDict): + offset: str + selector: str + + +class EntryPoints(TypedDict): + CONSTRUCTOR: List[EntryPoint] + EXTERNAL: List[EntryPoint] + L1_HANDLER: List[EntryPoint] + + +class RpcContractClass(TypedDict): + program: str + entry_point_by_type: EntryPoints + + +def rpc_contract_class(contract_class: ContractClass) -> RpcContractClass: + """ + Convert gateway contract class to rpc contract class + """ + def program() -> str: + prog: str = contract_class.program.Schema().dumps(contract_class.program) + prog_encoded = prog.encode("ascii") + prog_base64 = base64.b64encode(prog_encoded) + return prog_base64.decode() + + def entry_point_by_type() -> EntryPoints: + _entry_points = { + EntryPointType.CONSTRUCTOR: [], + EntryPointType.EXTERNAL: [], + EntryPointType.L1_HANDLER: [], + } + for typ, entry_points in contract_class.entry_points_by_type.items(): + for entry_point in entry_points: + _entry_point: EntryPoint = { + "selector": rpc_felt(entry_point.selector), + "offset": rpc_felt(entry_point.offset) + } + _entry_points[typ].append(_entry_point) + entry_points: EntryPoints = { + "CONSTRUCTOR": _entry_points[EntryPointType.CONSTRUCTOR], + "EXTERNAL": _entry_points[EntryPointType.EXTERNAL], + "L1_HANDLER": _entry_points[EntryPointType.L1_HANDLER], + } + return entry_points + + _contract_class: RpcContractClass = { + "program": program(), + "entry_point_by_type": entry_point_by_type() + } + return _contract_class + + class RpcBlock(TypedDict): """ TypeDict for rpc block @@ -686,6 +777,10 @@ def parse_body(body: dict) -> Tuple[Callable, Union[List, dict], int]: "protocolVersion": protocol_version, "syncing": syncing, "getEvents": get_events, + "getClass": get_class, + "getClassHashAt": get_class_hash_at, + "getClassAt": get_class_at, + "estimateFee": estimate_fee, } method_name = body["method"].lstrip("starknet_") diff --git a/test/test_rpc_endpoints.py b/test/test_rpc_endpoints.py index 91e13f52d..4bcc66eba 100644 --- a/test/test_rpc_endpoints.py +++ b/test/test_rpc_endpoints.py @@ -61,6 +61,14 @@ def fixture_contract_class() -> ContractClass: transaction: Deploy = typing.cast(Deploy, Transaction.loads(DEPLOY_CONTENT)) return transaction.contract_definition +@pytest.fixture(name="class_hash") +def fixture_class_hash(deploy_info) -> str: + """ + Class hash of deployed contract + """ + class_hash = gateway_call("get_class_hash_at", contractAddress=deploy_info["address"]) + return class_hash + @pytest.fixture(name="deploy_info", scope="module") def fixture_deploy_info() -> dict: @@ -830,3 +838,62 @@ def test_protocol_version(deploy_info): version = version_bytes.decode("utf-8") assert version == protocol_version + + +def test_get_class(class_hash): + """ + Test get contract class + """ + resp = rpc_call( + "starknet_getClass", + params={"class_hash": class_hash} + ) + contract_class = resp["result"] + + assert contract_class["entry_point_by_type"] == { + "CONSTRUCTOR": [], + "EXTERNAL": [ + {"offset": "0x03a", "selector": "0x0362398bec32bc0ebb411203221a35a0301193a96f317ebe5e40be9f60d15320"}, + {"offset": "0x05b", "selector": "0x039e11d48192e4333233c7eb19d10ad67c362bb28580c604d67884c85da39695"} + ], + "L1_HANDLER": [] + } + assert isinstance(contract_class["program"], str) + + +def test_get_class_hash_at(deploy_info, class_hash): + """ + Test get contract class at given hash + """ + contract_address: str = deploy_info["address"] + + resp = rpc_call( + "starknet_getClassHashAt", + params={"contract_address": contract_address} + ) + rpc_class_hash = resp["result"] + + assert rpc_class_hash == class_hash + + +def test_get_class_at(deploy_info): + """ + Test get contract class at given contract address + """ + contract_address: str = deploy_info["address"] + + resp = rpc_call( + "starknet_getClassAt", + params={"contract_address": contract_address} + ) + contract_class = resp["result"] + + assert contract_class["entry_point_by_type"] == { + "CONSTRUCTOR": [], + "EXTERNAL": [ + {"offset": "0x03a", "selector": "0x0362398bec32bc0ebb411203221a35a0301193a96f317ebe5e40be9f60d15320"}, + {"offset": "0x05b", "selector": "0x039e11d48192e4333233c7eb19d10ad67c362bb28580c604d67884c85da39695"} + ], + "L1_HANDLER": [] + } + assert isinstance(contract_class["program"], str) From b2f8f64b3925f623878f10bf55130fc5ab7dac30 Mon Sep 17 00:00:00 2001 From: Artur Michalek Date: Tue, 28 Jun 2022 12:41:00 +0200 Subject: [PATCH 2/9] Add new methods, update existing to 0.15 --- starknet_devnet/blueprints/rpc.py | 237 +++++++++++++++++++++++------- 1 file changed, 183 insertions(+), 54 deletions(-) diff --git a/starknet_devnet/blueprints/rpc.py b/starknet_devnet/blueprints/rpc.py index 29767ea1f..78f1c0edc 100644 --- a/starknet_devnet/blueprints/rpc.py +++ b/starknet_devnet/blueprints/rpc.py @@ -1,20 +1,20 @@ """ RPC routes -rpc version: 0.8.0 +rpc version: 0.15.0 """ from __future__ import annotations -import base64 import json from typing import Callable, Union, List, Tuple, Optional, Any -from starkware.starknet.services.api.contract_class import ContractClass, EntryPointType from typing_extensions import TypedDict from flask import Blueprint, request -from starkware.starknet.services.api.gateway.transaction import InvokeFunction +from starkware.starknet.services.api.contract_class import ContractClass, EntryPointType from starkware.starkware_utils.error_handling import StarkException +from starkware.starknet.services.api.gateway.transaction import InvokeFunction +from starkware.starknet.services.api.gateway.transaction_utils import compress_program from starkware.starknet.services.api.feeder_gateway.response_objects import ( StarknetBlock, InvokeSpecificInfo, @@ -23,7 +23,8 @@ TransactionStatus, TransactionSpecificInfo, TransactionType, - BlockStateUpdate + BlockStateUpdate, + DeclareSpecificInfo ) from starknet_devnet.state import state @@ -197,6 +198,9 @@ async def get_class(class_hash: str) -> dict: async def get_class_hash_at(contract_address: str) -> str: + """ + Get the contract class hash for the contract deployed at the given address + """ try: result = state.starknet_wrapper.contracts.get_class_hash_at(address=int(contract_address, 16)) except StarknetDevnetException as ex: @@ -206,6 +210,9 @@ async def get_class_hash_at(contract_address: str) -> str: async def get_class_at(contract_address: str) -> dict: + """ + Get the contract class definition at the given address + """ try: class_hash = state.starknet_wrapper.contracts.get_class_hash_at(address=int(contract_address, 16)) result = state.starknet_wrapper.contracts.get_class_by_hash(class_hash=class_hash) @@ -270,7 +277,10 @@ async def call(contract_address: str, entry_point_selector: str, calldata: list, async def estimate_fee(): - pass + """ + Get the estimate fee for the transaction + """ + raise NotImplementedError() async def get_block_number() -> int: @@ -335,19 +345,28 @@ def make_invoke_function(request_body: dict) -> InvokeFunction: class EntryPoint(TypedDict): + """ + TypedDict for rpc contract class entry point + """ offset: str selector: str class EntryPoints(TypedDict): + """ + TypedDict for rpc contract class entry points + """ CONSTRUCTOR: List[EntryPoint] EXTERNAL: List[EntryPoint] L1_HANDLER: List[EntryPoint] class RpcContractClass(TypedDict): + """ + TypedDict for rpc contract class + """ program: str - entry_point_by_type: EntryPoints + entry_points_by_type: EntryPoints def rpc_contract_class(contract_class: ContractClass) -> RpcContractClass: @@ -355,10 +374,8 @@ def rpc_contract_class(contract_class: ContractClass) -> RpcContractClass: Convert gateway contract class to rpc contract class """ def program() -> str: - prog: str = contract_class.program.Schema().dumps(contract_class.program) - prog_encoded = prog.encode("ascii") - prog_base64 = base64.b64encode(prog_encoded) - return prog_base64.decode() + _program = contract_class.program.Schema().dump(contract_class.program) + return compress_program(_program) def entry_point_by_type() -> EntryPoints: _entry_points = { @@ -382,7 +399,7 @@ def entry_point_by_type() -> EntryPoints: _contract_class: RpcContractClass = { "program": program(), - "entry_point_by_type": entry_point_by_type() + "entry_points_by_type": entry_point_by_type() } return _contract_class @@ -395,10 +412,9 @@ class RpcBlock(TypedDict): parent_hash: str block_number: int status: str - sequencer: str + sequencer_address: str new_root: str - old_root: str - accepted_time: int + timestamp: int transactions: List[str] | List[dict] @@ -406,7 +422,7 @@ async def rpc_block(block: StarknetBlock, requested_scope: Optional[str] = "TXN_ """ Convert gateway block to rpc block """ - async def transactions() -> List[RpcTransaction]: + async def transactions() -> List[Union[RpcInvokeTransaction, RpcDeclareTransaction]]: # pylint: disable=no-member return [rpc_transaction(tx) for tx in block.transactions] @@ -426,14 +442,6 @@ def new_root() -> str: # pylint: disable=no-member return rpc_root(block.state_root.hex()) - def old_root() -> str: - _root = state.starknet_wrapper.blocks.get_by_number(block_number=block_number - 1).state_root \ - if block_number - 1 >= 0 \ - else b"\x00" * 32 - return rpc_root(_root.hex()) - - block_number = block.block_number - mapping: dict[str, Callable] = { "TXN_HASH": transaction_hashes, "FULL_TXNS": transactions, @@ -449,10 +457,9 @@ def old_root() -> str: "parent_hash": rpc_felt(block.parent_block_hash) or "0x0", "block_number": block.block_number if block.block_number is not None else 0, "status": block.status.name, - "sequencer": hex(config.sequencer_address), + "sequencer_address": hex(config.sequencer_address), "new_root": new_root(), - "old_root": old_root(), - "accepted_time": block.timestamp, + "timestamp": block.timestamp, "transactions": transactions, } return block @@ -475,12 +482,21 @@ class RpcContractDiff(TypedDict): contract_hash: str +class RpcNonceDiff(TypedDict): + """ + TypedDict for rpc nonce diff + """ + contract_address: str + nonce: str + + class RpcStateDiff(TypedDict): """ - TypedDict for roc state diff + TypedDict for rpc state diff """ storage_diffs: List[RpcStorageDiff] contracts: List[RpcContractDiff] + nonces: List[RpcNonceDiff] class RpcStateUpdate(TypedDict): @@ -532,6 +548,7 @@ def timestamp() -> int: "state_diff": { "storage_diffs": storage_diffs(), "contracts": contracts(), + "nonces": [], } } return rpc_state @@ -558,59 +575,93 @@ def rpc_state_diff_storage(contract: dict) -> dict: } -class RpcTransaction(TypedDict): +class RpcInvokeTransaction(TypedDict): """ - TypedDict for rpc transaction + TypedDict for rpc invoke transaction """ contract_address: str entry_point_selector: Optional[str] calldata: Optional[List[str]] + # Common + txn_hash: str max_fee: str + version: str + signature: List[str] + + +class RpcDeclareTransaction(TypedDict): + """ + TypedDcit for rpc declare transaction + """ + contract_class: RpcContractClass + sender_address: str + # Common txn_hash: str + max_fee: str + version: str + signature: List[str] -def rpc_invoke_transaction(transaction: InvokeSpecificInfo) -> RpcTransaction: +def rpc_invoke_transaction(transaction: InvokeSpecificInfo) -> RpcInvokeTransaction: """ Convert gateway invoke transaction to rpc format """ - transaction: RpcTransaction = { + transaction: RpcInvokeTransaction = { "contract_address": rpc_felt(transaction.contract_address), "entry_point_selector": rpc_felt(transaction.entry_point_selector), "calldata": [rpc_felt(data) for data in transaction.calldata], "max_fee": rpc_felt(transaction.max_fee), "txn_hash": rpc_felt(transaction.transaction_hash), + "version": hex(0x0), + "signature": [rpc_felt(value) for value in transaction.signature] } return transaction -def rpc_deploy_transaction(transaction: DeploySpecificInfo) -> RpcTransaction: +def rpc_deploy_transaction(transaction: DeploySpecificInfo) -> RpcInvokeTransaction: """ Convert gateway deploy transaction to rpc format """ - def calldata() -> Optional[List[str]]: - # pylint: disable=no-member - _calldata = transaction.constructor_calldata - if not _calldata: - return None - return [rpc_felt(data) for data in _calldata] - - transaction: RpcTransaction = { + transaction: RpcInvokeTransaction = { "contract_address": rpc_felt(transaction.contract_address), "entry_point_selector": None, - "calldata": calldata(), - "max_fee": rpc_felt(0), + "calldata": [rpc_felt(data) for data in transaction.constructor_calldata], + "max_fee": rpc_felt(0x0), + "txn_hash": rpc_felt(transaction.transaction_hash), + "version": hex(0x0), + "signature": [] + } + return transaction + + +def rpc_declare_transaction(transaction: DeclareSpecificInfo) -> RpcDeclareTransaction: + """ + Convert gateway declare transaction to rpc format + """ + def contract_class() -> RpcContractClass: + # pylint: disable=no-member + _contract_claass = state.starknet_wrapper.contracts.get_class_by_hash(transaction.class_hash) + return rpc_contract_class(_contract_claass) + + transaction: RpcDeclareTransaction = { + "contract_class": contract_class(), + "sender_address": rpc_felt(transaction.sender_address), + "max_fee": rpc_felt(transaction.max_fee), "txn_hash": rpc_felt(transaction.transaction_hash), + "version": hex(transaction.version), + "signature": [rpc_felt(value) for value in transaction.signature] } return transaction -def rpc_transaction(transaction: TransactionSpecificInfo) -> RpcTransaction: +def rpc_transaction(transaction: TransactionSpecificInfo) -> Union[RpcInvokeTransaction, RpcDeclareTransaction]: """ Convert gateway transaction to rpc transaction """ tx_mapping = { TransactionType.DEPLOY: rpc_deploy_transaction, - TransactionType.INVOKE_FUNCTION: rpc_invoke_transaction + TransactionType.INVOKE_FUNCTION: rpc_invoke_transaction, + TransactionType.DECLARE: rpc_declare_transaction, } return tx_mapping[transaction.tx_type](transaction) @@ -640,22 +691,45 @@ class Event(TypedDict): data: List[str] -class RpcReceipt(TypedDict): +class RpcBaseTransactionReceipt(TypedDict): """ TypedDict for rpc transaction receipt """ + # Common txn_hash: str actual_fee: str status: str - statusData: str + statusData: Optional[str] + + +class RpcInvokeReceipt(TypedDict): + """ + TypedDict for rpc invoke transaction receipt + """ messages_sent: List[MessageToL1] l1_origin_message: Optional[MessageToL2] events: List[Event] + # Common + txn_hash: str + actual_fee: str + status: str + statusData: Optional[str] -def rpc_transaction_receipt(txr: TransactionReceipt) -> RpcReceipt: +class RpcDeclareReceipt(TypedDict): """ - Convert gateway transaction receipt to rpc transaction receipt + TypedDict for rpc declare transaction receipt + """ + # Common + txn_hash: str + actual_fee: str + status: str + statusData: Optional[str] + + +def rpc_invoke_receipt(txr: TransactionReceipt) -> RpcInvokeReceipt: + """ + Convert rpc invoke transaction receipt to rpc format """ def l2_to_l1_messages() -> List[MessageToL1]: messages = [] @@ -688,6 +762,44 @@ def events() -> List[Event]: _events.append(event) return _events + base_receipt = rpc_base_transaction_receipt(txr) + receipt: RpcInvokeReceipt = { + "messages_sent": l2_to_l1_messages(), + "l1_origin_message": l1_to_l2_message(), + "events": events(), + "txn_hash": base_receipt["txn_hash"], + "status": base_receipt["status"], + "statusData": base_receipt["statusData"], + "actual_fee": base_receipt["actual_fee"], + } + return receipt + + +def rpc_declare_receipt(txr) -> RpcDeclareReceipt: + """ + Conver rpc declare transaction receipt to rpc format + """ + base_receipt = rpc_base_transaction_receipt(txr) + receipt: RpcDeclareReceipt = { + "txn_hash": base_receipt["txn_hash"], + "status": base_receipt["status"], + "statusData": base_receipt["statusData"], + "actual_fee": base_receipt["actual_fee"], + } + return receipt + + +def rpc_deploy_receipt(txr) -> RpcBaseTransactionReceipt: + """ + Conver rpc deploy transaction receipt to rpc format + """ + return rpc_base_transaction_receipt(txr) + + +def rpc_base_transaction_receipt(txr: TransactionReceipt) -> RpcBaseTransactionReceipt: + """ + Convert gateway transaction receipt to rpc transaction receipt + """ def status() -> str: if txr.status is None: return "UNKNOWN" @@ -702,18 +814,35 @@ def status() -> str: } return mapping[txr.status] - receipt: RpcReceipt = { + def status_data() -> Union[str, None]: + if txr.transaction_failure_reason is not None: + if txr.transaction_failure_reason.error_message is not None: + return txr.transaction_failure_reason.error_message + return None + + receipt: RpcBaseTransactionReceipt = { "txn_hash": rpc_felt(txr.transaction_hash), "status": status(), - "statusData": "", - "messages_sent": l2_to_l1_messages(), - "l1_origin_message": l1_to_l2_message(), - "events": events(), + "statusData": status_data(), "actual_fee": rpc_felt(txr.actual_fee or 0), } return receipt +def rpc_transaction_receipt(txr: TransactionReceipt) -> dict: + """ + Convert gateway transaction receipt to rpc format + """ + tx_mapping = { + TransactionType.DEPLOY: rpc_deploy_receipt, + TransactionType.INVOKE_FUNCTION: rpc_invoke_receipt, + TransactionType.DECLARE: rpc_declare_receipt, + } + transaction = state.starknet_wrapper.transactions.get_transaction(hex(txr.transaction_hash)).transaction + tx_type = transaction.tx_type + return tx_mapping[tx_type](txr) + + def rpc_response(message_id: int, content: dict) -> dict: """ Wrap response content in rpc format From 428f24942c7052fcad07818a846e1890e70ae0ec Mon Sep 17 00:00:00 2001 From: Artur Michalek Date: Tue, 28 Jun 2022 12:41:12 +0200 Subject: [PATCH 3/9] Update tests --- test/declare.json | 49 +++++++++++ test/test_rpc_endpoints.py | 172 +++++++++++++++++++++++++++++++------ 2 files changed, 197 insertions(+), 24 deletions(-) create mode 100644 test/declare.json diff --git a/test/declare.json b/test/declare.json new file mode 100644 index 000000000..413fe3b65 --- /dev/null +++ b/test/declare.json @@ -0,0 +1,49 @@ +{ + "sender_address": "0x1", + "max_fee": "0x0", + "signature": [], + "nonce": "0x0", + "type": "DECLARE", + "contract_class": { + "program": "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", + "abi": [ + { + "inputs": [ + { + "name": "amount", + "type": "felt" + } + ], + "name": "increase_balance", + "outputs": [], + "type": "function" + }, + { + "inputs": [], + "name": "get_balance", + "outputs": [ + { + "name": "res", + "type": "felt" + } + ], + "stateMutability": "view", + "type": "function" + } + ], + "entry_points_by_type": { + "CONSTRUCTOR": [], + "EXTERNAL": [ + { + "offset": "0x3a", + "selector": "0x362398bec32bc0ebb411203221a35a0301193a96f317ebe5e40be9f60d15320" + }, + { + "offset": "0x5b", + "selector": "0x39e11d48192e4333233c7eb19d10ad67c362bb28580c604d67884c85da39695" + } + ], + "L1_HANDLER": [] + } + } +} \ No newline at end of file diff --git a/test/test_rpc_endpoints.py b/test/test_rpc_endpoints.py index 4bcc66eba..fe0da4b3f 100644 --- a/test/test_rpc_endpoints.py +++ b/test/test_rpc_endpoints.py @@ -1,10 +1,12 @@ """ Tests RPC endpoints. """ +# pylint: disable=too-many-lines from __future__ import annotations import json import typing +from typing import List import pytest from starkware.starknet.public.abi import get_storage_var_address, get_selector_from_name @@ -21,6 +23,7 @@ DEPLOY_CONTENT = load_file_content("deploy_rpc.json") INVOKE_CONTENT = load_file_content("invoke_rpc.json") +DECLARE_CONTENT = load_file_content("declare.json") def rpc_call(method: str, params: dict | list) -> dict: @@ -61,6 +64,7 @@ def fixture_contract_class() -> ContractClass: transaction: Deploy = typing.cast(Deploy, Transaction.loads(DEPLOY_CONTENT)) return transaction.contract_definition + @pytest.fixture(name="class_hash") def fixture_class_hash(deploy_info) -> str: """ @@ -89,7 +93,18 @@ def fixture_invoke_info() -> dict: invoke_tx["calldata"] = ["0"] resp = send_transaction(invoke_tx) invoke_info = json.loads(resp.data.decode("utf-8")) - return invoke_info + return {**invoke_info, **invoke_tx} + + +@pytest.fixture(name="declare_info", scope="module") +def fixture_declare_info() -> dict: + """ + Make a declare transaction on devnet and return declare info dict + """ + declare_tx = json.loads(DECLARE_CONTENT) + resp = send_transaction(declare_tx) + declare_info = json.loads(resp.data.decode("utf-8")) + return {**declare_info, **declare_tx} def get_block_with_transaction(transaction_hash: str) -> dict: @@ -130,8 +145,7 @@ def test_get_block_by_number(deploy_info): assert block["parent_hash"] == pad_zero(gateway_block["parent_block_hash"]) assert block["block_number"] == block_number assert block["status"] == "ACCEPTED_ON_L2" - assert block["sequencer"] == hex(DEFAULT_GENERAL_CONFIG.sequencer_address) - assert block["old_root"] != "" + assert block["sequencer_address"] == hex(DEFAULT_GENERAL_CONFIG.sequencer_address) assert block["new_root"] == pad_zero(new_root) assert block["transactions"] == [transaction_hash] @@ -168,8 +182,7 @@ def test_get_block_by_hash(deploy_info): assert block["parent_hash"] == pad_zero(gateway_block["parent_block_hash"]) assert block["block_number"] == gateway_block["block_number"] assert block["status"] == "ACCEPTED_ON_L2" - assert block["sequencer"] == hex(DEFAULT_GENERAL_CONFIG.sequencer_address) - assert block["old_root"] != "" + assert block["sequencer_address"] == hex(DEFAULT_GENERAL_CONFIG.sequencer_address) assert block["new_root"] == pad_zero(new_root) assert block["transactions"] == [transaction_hash] @@ -195,8 +208,10 @@ def test_get_block_by_hash_full_txn_scope(deploy_info): "txn_hash": transaction_hash, "max_fee": "0x0", "contract_address": contract_address, - "calldata": None, + "calldata": [], "entry_point_selector": None, + "signature": [], + "version": "0x0" }] @@ -221,14 +236,13 @@ def test_get_block_by_hash_full_txn_and_receipts_scope(deploy_info): "txn_hash": transaction_hash, "max_fee": "0x0", "contract_address": contract_address, - "calldata": None, + "calldata": [], "entry_point_selector": None, + "signature": [], + "version": "0x0", "actual_fee": "0x0", "status": "ACCEPTED_ON_L2", - "statusData": "", - "messages_sent": [], - "l1_origin_message": None, - "events": [] + "statusData": None, }] @@ -282,6 +296,7 @@ def test_get_state_update_by_hash(deploy_info, invoke_info, contract_class): "contract_hash": pad_zero(hex(compute_class_hash(contract_class))), } ], + "nonces": [], } storage = gateway_call("get_storage_at", contractAddress=contract_address, key=get_storage_var_address("balance")) @@ -306,6 +321,7 @@ def test_get_state_update_by_hash(deploy_info, invoke_info, contract_class): } ], "contracts": [], + "nonces": [], } @@ -416,12 +432,76 @@ def test_get_transaction_by_hash_deploy(deploy_info): assert transaction == { "txn_hash": pad_zero(transaction_hash), "contract_address": contract_address, + "max_fee": "0x0", + "calldata": [], "entry_point_selector": None, - "calldata": None, - "max_fee": "0x0" + "signature": [], + "version": "0x0" } +def test_get_transaction_by_hash_invoke(invoke_info): + """ + Get transaction by hash + """ + transaction_hash: str = invoke_info["transaction_hash"] + contract_address: str = invoke_info["address"] + entry_point_selector: str = invoke_info["entry_point_selector"] + signature: List[str] = [pad_zero(hex(int(sig))) for sig in invoke_info["signature"]] + calldata: List[str] = [pad_zero(hex(int(data))) for data in invoke_info["calldata"]] + + resp = rpc_call( + "starknet_getTransactionByHash", params={"transaction_hash": transaction_hash} + ) + transaction = resp["result"] + + assert transaction == { + "txn_hash": pad_zero(transaction_hash), + "contract_address": contract_address, + "max_fee": "0x0", + "calldata": calldata, + "entry_point_selector": pad_zero(entry_point_selector), + "signature": signature, + "version": "0x0" + } + + +def test_get_transaction_by_hash_declare(declare_info): + """ + Get transaction by hash + """ + transaction_hash: str = declare_info["transaction_hash"] + signature: List[str] = [pad_zero(hex(int(sig))) for sig in declare_info["signature"]] + sender_address: str = declare_info["sender_address"] + + resp = rpc_call( + "starknet_getTransactionByHash", params={"transaction_hash": transaction_hash} + ) + transaction = resp["result"] + + assert transaction["txn_hash"] == pad_zero(transaction_hash) + assert transaction["max_fee"] == "0x0" + assert transaction["signature"] == signature + assert transaction["version"] == "0x0" + assert transaction["sender_address"] == pad_zero(sender_address) + assert transaction["contract_class"]["entry_points_by_type"] == { + "CONSTRUCTOR": [], + "EXTERNAL": [ + { + "offset": pad_zero("0x3a"), + "selector": pad_zero("0x362398bec32bc0ebb411203221a35a0301193a96f317ebe5e40be9f60d15320") + }, + { + "offset": pad_zero("0x5b"), + "selector": pad_zero("0x39e11d48192e4333233c7eb19d10ad67c362bb28580c604d67884c85da39695") + } + ], + "L1_HANDLER": [] + } + assert transaction["contract_class"]["program"] != "" + + + def test_get_transaction_by_hash_raises_on_incorrect_hash(deploy_info): """ Get transaction by incorrect hash @@ -457,9 +537,11 @@ def test_get_transaction_by_block_hash_and_index(deploy_info): assert transaction == { "txn_hash": pad_zero(transaction_hash), "contract_address": contract_address, - "entry_point_selector": None, - "calldata": None, "max_fee": "0x0", + "calldata": [], + "entry_point_selector": None, + "signature": [], + "version": "0x0" } @@ -521,9 +603,11 @@ def test_get_transaction_by_block_number_and_index(deploy_info): assert transaction == { "txn_hash": pad_zero(transaction_hash), "contract_address": contract_address, - "entry_point_selector": None, - "calldata": None, "max_fee": "0x0", + "calldata": [], + "entry_point_selector": None, + "signature": [], + "version": "0x0" } @@ -563,7 +647,7 @@ def test_get_transaction_by_block_number_and_index_raises_on_incorrect_index(dep } -def test_get_transaction_receipt(deploy_info, invoke_info): +def test_get_deploy_transaction_receipt(deploy_info): """ Get transaction receipt """ @@ -579,14 +663,54 @@ def test_get_transaction_receipt(deploy_info, invoke_info): assert receipt == { "txn_hash": pad_zero(transaction_hash), "status": "ACCEPTED_ON_L2", - "statusData": "", - "messages_sent": [], - "l1_origin_message": None, - "events": [], + "statusData": None, + "actual_fee": "0x0" + } + + +def test_get_declare_transaction_receipt(declare_info): + """ + Get transaction receipt + """ + transaction_hash: str = declare_info["transaction_hash"] + + resp = rpc_call( + "starknet_getTransactionReceipt", params={ + "transaction_hash": transaction_hash + } + ) + receipt = resp["result"] + + assert receipt == { + "txn_hash": pad_zero(transaction_hash), + "status": "ACCEPTED_ON_L2", + "statusData": None, "actual_fee": "0x0" } +def test_get_invoke_transaction_receipt(invoke_info): + """ + Get transaction receipt + """ + transaction_hash: str = invoke_info["transaction_hash"] + + resp = rpc_call( + "starknet_getTransactionReceipt", params={ + "transaction_hash": transaction_hash + } + ) + receipt = resp["result"] + + # Standard == receipt dict test cannot be done here, because invoke transaction fails since no contracts + # are actually deployed on devnet, when running test without @devnet_in_background + assert receipt["txn_hash"] == pad_zero(transaction_hash) + assert receipt["actual_fee"] == "0x0" + assert receipt["l1_origin_message"] is None + assert receipt["events"] == [] + assert receipt["messages_sent"] == [] + + def test_get_transaction_receipt_on_incorrect_hash(deploy_info): """ Get transaction receipt by incorrect hash @@ -850,7 +974,7 @@ def test_get_class(class_hash): ) contract_class = resp["result"] - assert contract_class["entry_point_by_type"] == { + assert contract_class["entry_points_by_type"] == { "CONSTRUCTOR": [], "EXTERNAL": [ {"offset": "0x03a", "selector": "0x0362398bec32bc0ebb411203221a35a0301193a96f317ebe5e40be9f60d15320"}, @@ -888,7 +1012,7 @@ def test_get_class_at(deploy_info): ) contract_class = resp["result"] - assert contract_class["entry_point_by_type"] == { + assert contract_class["entry_points_by_type"] == { "CONSTRUCTOR": [], "EXTERNAL": [ {"offset": "0x03a", "selector": "0x0362398bec32bc0ebb411203221a35a0301193a96f317ebe5e40be9f60d15320"}, From dde7820d6173aa6dadfb6b8ae69df765f85eeac2 Mon Sep 17 00:00:00 2001 From: Artur Michalek Date: Tue, 28 Jun 2022 13:37:31 +0200 Subject: [PATCH 4/9] Update protocol version --- starknet_devnet/blueprints/rpc.py | 2 +- test/test_rpc_endpoints.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/starknet_devnet/blueprints/rpc.py b/starknet_devnet/blueprints/rpc.py index 78f1c0edc..a859fe526 100644 --- a/starknet_devnet/blueprints/rpc.py +++ b/starknet_devnet/blueprints/rpc.py @@ -32,7 +32,7 @@ rpc = Blueprint("rpc", __name__, url_prefix="/rpc") -PROTOCOL_VERSION = "0.8.0" +PROTOCOL_VERSION = "0.15.0" @rpc.route("", methods=["POST"]) async def base_route(): diff --git a/test/test_rpc_endpoints.py b/test/test_rpc_endpoints.py index fe0da4b3f..f14f26a43 100644 --- a/test/test_rpc_endpoints.py +++ b/test/test_rpc_endpoints.py @@ -954,7 +954,7 @@ def test_protocol_version(deploy_info): """ Test protocol version """ - protocol_version = "0.8.0" + protocol_version = "0.15.0" resp = rpc_call("starknet_protocolVersion", params={}) version_hex: str = resp["result"] From d14666603fd4cc704d3288243dc88d20fb8c79fc Mon Sep 17 00:00:00 2001 From: Artur Michalek Date: Thu, 30 Jun 2022 09:29:23 +0200 Subject: [PATCH 5/9] Simplify entry_points_by_type --- starknet_devnet/blueprints/rpc.py | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/starknet_devnet/blueprints/rpc.py b/starknet_devnet/blueprints/rpc.py index a859fe526..76a38ccc9 100644 --- a/starknet_devnet/blueprints/rpc.py +++ b/starknet_devnet/blueprints/rpc.py @@ -377,11 +377,11 @@ def program() -> str: _program = contract_class.program.Schema().dump(contract_class.program) return compress_program(_program) - def entry_point_by_type() -> EntryPoints: - _entry_points = { - EntryPointType.CONSTRUCTOR: [], - EntryPointType.EXTERNAL: [], - EntryPointType.L1_HANDLER: [], + def entry_points_by_type() -> EntryPoints: + _entry_points: EntryPoints = { + "CONSTRUCTOR": [], + "EXTERNAL": [], + "L1_HANDLER": [], } for typ, entry_points in contract_class.entry_points_by_type.items(): for entry_point in entry_points: @@ -389,17 +389,12 @@ def entry_point_by_type() -> EntryPoints: "selector": rpc_felt(entry_point.selector), "offset": rpc_felt(entry_point.offset) } - _entry_points[typ].append(_entry_point) - entry_points: EntryPoints = { - "CONSTRUCTOR": _entry_points[EntryPointType.CONSTRUCTOR], - "EXTERNAL": _entry_points[EntryPointType.EXTERNAL], - "L1_HANDLER": _entry_points[EntryPointType.L1_HANDLER], - } - return entry_points + _entry_points[typ.name].append(_entry_point) + return _entry_points _contract_class: RpcContractClass = { "program": program(), - "entry_points_by_type": entry_point_by_type() + "entry_points_by_type": entry_points_by_type() } return _contract_class From 90359f53c99793bbd58d6b186ac3470c4e0c74c7 Mon Sep 17 00:00:00 2001 From: Artur Michalek Date: Thu, 30 Jun 2022 09:30:40 +0200 Subject: [PATCH 6/9] Replace `|` with Union --- starknet_devnet/blueprints/rpc.py | 2 +- test/test_rpc_endpoints.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/starknet_devnet/blueprints/rpc.py b/starknet_devnet/blueprints/rpc.py index 76a38ccc9..9f785084f 100644 --- a/starknet_devnet/blueprints/rpc.py +++ b/starknet_devnet/blueprints/rpc.py @@ -410,7 +410,7 @@ class RpcBlock(TypedDict): sequencer_address: str new_root: str timestamp: int - transactions: List[str] | List[dict] + transactions: Union[List[str], List[dict]] async def rpc_block(block: StarknetBlock, requested_scope: Optional[str] = "TXN_HASH") -> RpcBlock: diff --git a/test/test_rpc_endpoints.py b/test/test_rpc_endpoints.py index f14f26a43..8606dfd84 100644 --- a/test/test_rpc_endpoints.py +++ b/test/test_rpc_endpoints.py @@ -6,7 +6,7 @@ import json import typing -from typing import List +from typing import List, Union import pytest from starkware.starknet.public.abi import get_storage_var_address, get_selector_from_name @@ -26,7 +26,7 @@ DECLARE_CONTENT = load_file_content("declare.json") -def rpc_call(method: str, params: dict | list) -> dict: +def rpc_call(method: str, params: Union[dict, list]) -> dict: """ Make a call to the RPC endpoint """ From b0bd785cb339ffd2522c1395fe96abfaef773d8c Mon Sep 17 00:00:00 2001 From: Artur Michalek Date: Thu, 30 Jun 2022 09:31:02 +0200 Subject: [PATCH 7/9] Add newline --- test/declare.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/declare.json b/test/declare.json index 413fe3b65..b0f608da2 100644 --- a/test/declare.json +++ b/test/declare.json @@ -46,4 +46,4 @@ "L1_HANDLER": [] } } -} \ No newline at end of file +} From b5e1255851ecee3ff74588297fe5ba3446072c27 Mon Sep 17 00:00:00 2001 From: Artur Michalek Date: Thu, 30 Jun 2022 09:39:23 +0200 Subject: [PATCH 8/9] Remove extra newline --- test/test_rpc_endpoints.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test/test_rpc_endpoints.py b/test/test_rpc_endpoints.py index 8606dfd84..455fdc94a 100644 --- a/test/test_rpc_endpoints.py +++ b/test/test_rpc_endpoints.py @@ -501,7 +501,6 @@ def test_get_transaction_by_hash_declare(declare_info): assert transaction["contract_class"]["program"] != "" - def test_get_transaction_by_hash_raises_on_incorrect_hash(deploy_info): """ Get transaction by incorrect hash From 8af730408f10716b4d419b2d11a376522f327cc1 Mon Sep 17 00:00:00 2001 From: Artur Michalek Date: Thu, 30 Jun 2022 09:53:27 +0200 Subject: [PATCH 9/9] Update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8e0be3111..48a8fb00e 100644 --- a/README.md +++ b/README.md @@ -158,7 +158,7 @@ If you don't specify the `HOST` part, the server will indeed be available on all ## JSON-RPC API -Devnet also supports JSON-RPC API (v0.8.0: [specifications](https://github.com/starkware-libs/starknet-specs/blob/ec01ba5fd12d4a51a9202146a2d6247eebc08644/api/starknet_api_openrpc.json)). It can be reached under `/rpc`. For an example: +Devnet also partially supports JSON-RPC API (v0.15.0: [specifications](https://github.com/starkware-libs/starknet-specs/blob/606c21e06be92ea1543fd0134b7f98df622c2fbf/api/starknet_api_openrpc.json)). It can be reached under `/rpc`. For an example: ``` POST /rpc