Skip to content

Commit

Permalink
Allow RPC calls using block hash or number of latest block (0xSpaceSh…
Browse files Browse the repository at this point in the history
  • Loading branch information
cptartur committed Oct 21, 2022
1 parent 923d3da commit 715612b
Show file tree
Hide file tree
Showing 12 changed files with 145 additions and 33 deletions.
29 changes: 29 additions & 0 deletions page/docs/guide/json-rpc-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,32 @@ Response:
Methods currently not supported:

- `starknet_protocolVersion` - will be removed in a future version of the specification

Methods that require a `block_id` only support ids of the `latest` or `pending` block.
Please note however, that the `pending` block will be the same block as the `latest`.

```js
// Use latest
{
"block_id": "latest"
}

// or pending
{
"block_id": "pending"
}

// or block number
{
"block_id": {
"block_number": 1234 // Must be the number of the latest block
}
}

// or block hash
{
"block_id": {
"block_hash": "0x1234" // Must be hash of the latest block
}
}
```
6 changes: 3 additions & 3 deletions starknet_devnet/blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def __init__(self, origin: Origin, lite=False) -> None:
self.__state_updates: Dict[int, BlockStateUpdate] = {}
self.__hash2num: Dict[str, int] = {}

def __get_last_block(self):
def get_last_block(self) -> StarknetBlock:
"""Returns the last block stored so far."""
number_of_blocks = self.get_number_of_blocks()
return self.get_by_number(number_of_blocks - 1)
Expand All @@ -46,7 +46,7 @@ def get_by_number(self, block_number: int) -> StarknetBlock:
"""Returns the block whose block_number is provided"""
if block_number is None:
if self.__num2block:
return self.__get_last_block()
return self.get_last_block()
return self.origin.get_block_by_number(block_number)

if block_number < 0:
Expand Down Expand Up @@ -122,7 +122,7 @@ async def generate(
if block_number == 0:
parent_block_hash = 0
else:
last_block = self.__get_last_block()
last_block = self.get_last_block()
parent_block_hash = last_block.block_hash

if is_empty_block:
Expand Down
7 changes: 5 additions & 2 deletions starknet_devnet/blueprints/rpc/call.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@

from starkware.starkware_utils.error_handling import StarkException

from starknet_devnet.blueprints.rpc.utils import rpc_felt, assert_block_id_is_latest
from starknet_devnet.blueprints.rpc.utils import (
rpc_felt,
assert_block_id_is_latest_or_pending,
)
from starknet_devnet.blueprints.rpc.structures.payloads import (
make_invoke_function,
FunctionCall,
Expand All @@ -33,7 +36,7 @@ async def call(request: FunctionCall, block_id: BlockId) -> List[Felt]:
"""
Call a starknet function without creating a StarkNet transaction
"""
assert_block_id_is_latest(block_id)
assert_block_id_is_latest_or_pending(block_id)

if not state.starknet_wrapper.contracts.is_deployed(
int(request["contract_address"], 16)
Expand Down
9 changes: 6 additions & 3 deletions starknet_devnet/blueprints/rpc/classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
RPC classes endpoints
"""

from starknet_devnet.blueprints.rpc.utils import assert_block_id_is_latest, rpc_felt
from starknet_devnet.blueprints.rpc.utils import (
assert_block_id_is_latest_or_pending,
rpc_felt,
)
from starknet_devnet.blueprints.rpc.structures.payloads import rpc_contract_class
from starknet_devnet.blueprints.rpc.structures.types import (
BlockId,
Expand Down Expand Up @@ -34,7 +37,7 @@ async def get_class_hash_at(block_id: BlockId, contract_address: Address) -> Fel
"""
Get the contract class hash in the given block for the contract deployed at the given address
"""
assert_block_id_is_latest(block_id)
assert_block_id_is_latest_or_pending(block_id)

try:
result = state.starknet_wrapper.contracts.get_class_hash_at(
Expand All @@ -52,7 +55,7 @@ async def get_class_at(block_id: BlockId, contract_address: Address) -> dict:
"""
Get the contract class definition in the given block at the given address
"""
assert_block_id_is_latest(block_id)
assert_block_id_is_latest_or_pending(block_id)

try:
class_hash = state.starknet_wrapper.contracts.get_class_hash_at(
Expand Down
7 changes: 5 additions & 2 deletions starknet_devnet/blueprints/rpc/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
RPC storage endpoints
"""

from starknet_devnet.blueprints.rpc.utils import assert_block_id_is_latest, rpc_felt
from starknet_devnet.blueprints.rpc.utils import (
assert_block_id_is_latest_or_pending,
rpc_felt,
)
from starknet_devnet.blueprints.rpc.structures.types import (
Address,
BlockId,
Expand All @@ -18,7 +21,7 @@ async def get_storage_at(
"""
Get the value of the storage at the given address and key
"""
assert_block_id_is_latest(block_id)
assert_block_id_is_latest_or_pending(block_id)

if not state.starknet_wrapper.contracts.is_deployed(int(contract_address, 16)):
raise RpcError(code=20, message="Contract not found")
Expand Down
4 changes: 2 additions & 2 deletions starknet_devnet/blueprints/rpc/transactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from starknet_devnet.blueprints.rpc.utils import (
get_block_by_block_id,
rpc_felt,
assert_block_id_is_latest,
assert_block_id_is_latest_or_pending,
)
from starknet_devnet.blueprints.rpc.structures.payloads import (
rpc_transaction,
Expand Down Expand Up @@ -212,7 +212,7 @@ async def estimate_fee(request: RpcInvokeTransaction, block_id: BlockId) -> dict
"""
Estimate the fee for a given StarkNet transaction
"""
assert_block_id_is_latest(block_id)
assert_block_id_is_latest_or_pending(block_id)

if not state.starknet_wrapper.contracts.is_deployed(
int(request["contract_address"], 16)
Expand Down
40 changes: 32 additions & 8 deletions starknet_devnet/blueprints/rpc/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@
RPC utilities
"""

from starknet_devnet.blueprints.rpc.structures.types import BlockId, RpcError, Felt
from starknet_devnet.blueprints.rpc.structures.types import (
BlockId,
RpcError,
Felt,
)
from starknet_devnet.util import StarknetDevnetException
from starknet_devnet.state import state

Expand Down Expand Up @@ -47,15 +51,35 @@ def get_block_by_block_id(block_id: BlockId) -> dict:
raise RpcError(code=24, message="Invalid block id") from ex


def assert_block_id_is_latest(block_id: BlockId) -> None:
def assert_block_id_is_latest_or_pending(block_id: BlockId) -> None:
"""
Assert block_id is "latest" and throw RpcError otherwise
Assert block_id is "latest"/"pending" or a block hash or number of "latest"/"pending" block and throw RpcError otherwise
"""
if block_id != "latest":
raise RpcError(
code=-1,
message="Calls with block_id != 'latest' are not supported currently.",
)
if isinstance(block_id, dict):
last_block = state.starknet_wrapper.blocks.get_last_block()

if "block_hash" in block_id and "block_number" in block_id:
raise RpcError(
code=-1,
message="Parameters block_hash and block_number are mutually exclusive.",
)

if "block_hash" in block_id:
if int(block_id["block_hash"], 16) == last_block.block_hash:
return

if "block_number" in block_id:
if int(block_id["block_number"]) == last_block.block_number:
return

if isinstance(block_id, str):
if block_id in ("latest", "pending"):
return

raise RpcError(
code=-1,
message="Calls must be made with block_id of the latest or pending block. Other block_id are not supported.",
)


def rpc_felt(value: int) -> Felt:
Expand Down
34 changes: 28 additions & 6 deletions test/rpc/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
get_block_with_transaction,
pad_zero,
add_transaction,
get_latest_block,
)

DEPLOY_CONTENT = load_file_content("deploy_rpc.json")
Expand Down Expand Up @@ -119,17 +120,38 @@ def fixture_gateway_block(deploy_info) -> dict:
return get_block_with_transaction(deploy_info["transaction_hash"])


@pytest.fixture(name="block_id")
def fixture_block_id(gateway_block, request) -> dict:
@pytest.fixture(name="latest_block")
def fixture_latest_block() -> dict:
"""
BlockId of gateway_block depending on type in request
Latest block
"""
return get_latest_block()


def _block_to_block_id(block: dict, key: str) -> dict:
block_id_map = {
"hash": BlockNumberDict(block_number=gateway_block["block_number"]),
"number": BlockHashDict(block_hash=pad_zero(gateway_block["block_hash"])),
"number": BlockNumberDict(block_number=int(block["block_number"])),
"hash": BlockHashDict(block_hash=pad_zero(block["block_hash"])),
"tag": "latest",
"tag_pending": "pending",
}
return block_id_map[request.param]
return block_id_map[key]


@pytest.fixture(name="block_id", params=["hash", "number", "tag"])
def fixture_block_id(gateway_block, request) -> dict:
"""
BlockId of gateway_block depending on type in request
"""
return _block_to_block_id(gateway_block, request.param)


@pytest.fixture(name="latest_block_id", params=["hash", "number", "tag", "tag_pending"])
def fixture_latest_block_id(latest_block, request) -> dict:
"""
Parametrized BlockId of latest gateway_block
"""
return _block_to_block_id(latest_block, request.param)


@pytest.fixture(name="rpc_invoke_tx_common")
Expand Down
7 changes: 7 additions & 0 deletions test/rpc/rpc_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,13 @@ def get_block_with_transaction(transaction_hash: str) -> dict:
return block


def get_latest_block() -> dict:
"""
Retrive the latest block
"""
return gateway_call("get_block", blockNumber="latest")


def pad_zero(felt: str) -> Felt:
"""
Convert felt with format `0xValue` to format `0x0Value`
Expand Down
3 changes: 0 additions & 3 deletions test/rpc/test_rpc_blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@


@pytest.mark.usefixtures("run_devnet_in_background")
@pytest.mark.parametrize("block_id", ["hash", "number", "tag"], indirect=True)
def test_get_block_with_tx_hashes(deploy_info, gateway_block, block_id):
"""
Get block with tx hashes
Expand Down Expand Up @@ -63,7 +62,6 @@ def test_get_block_with_tx_hashes_raises_on_incorrect_block_id(block_id):


@pytest.mark.usefixtures("run_devnet_in_background", "deploy_info")
@pytest.mark.parametrize("block_id", ["hash", "number", "tag"], indirect=True)
def test_get_block_with_txs(gateway_block, block_id):
"""
Get block with txs by block id
Expand Down Expand Up @@ -118,7 +116,6 @@ def test_get_block_with_txs_raises_on_incorrect_block_id(block_id):


@pytest.mark.usefixtures("run_devnet_in_background", "deploy_info", "gateway_block")
@pytest.mark.parametrize("block_id", ["hash", "number", "tag"], indirect=True)
def test_get_block_transaction_count(block_id):
"""
Get count of transactions in block by block id
Expand Down
30 changes: 27 additions & 3 deletions test/rpc/test_rpc_call.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@


@pytest.mark.usefixtures("run_devnet_in_background")
def test_call(deploy_info):
def test_call(deploy_info, latest_block_id):
"""
Call contract
"""
Expand All @@ -23,9 +23,10 @@ def test_call(deploy_info):
"entry_point_selector": hex(get_selector_from_name("get_balance")),
"calldata": [],
},
"block_id": "latest",
"block_id": latest_block_id,
},
)
assert "error" not in resp
result = resp["result"]

assert result == ["0x045"]
Expand All @@ -51,6 +52,29 @@ def test_call_raises_on_incorrect_contract_address():
assert ex["error"] == {"code": 20, "message": "Contract not found"}


@pytest.mark.usefixtures("run_devnet_in_background", "deploy_info")
def test_call_raises_on_both_hash_and_number():
"""
Call contract with both block hash and block number provided at the same time
"""
ex = rpc_call(
"starknet_call",
params={
"request": {
"contract_address": "0x07b529269b82f3f3ebbb2c463a9e1edaa2c6eea8fa308ff70b30398766a2e20c",
"entry_point_selector": hex(get_selector_from_name("get_balance")),
"calldata": [],
},
"block_id": {"block_hash": "0x1234", "block_number": 1234},
},
)

assert ex["error"] == {
"code": -1,
"message": "Parameters block_hash and block_number are mutually exclusive.",
}


@pytest.mark.usefixtures("run_devnet_in_background")
def test_call_raises_on_incorrect_selector(deploy_info):
"""
Expand Down Expand Up @@ -120,7 +144,7 @@ def test_call_raises_on_incorrect_block_hash(deploy_info):

assert ex["error"] == {
"code": -1,
"message": "Calls with block_id != 'latest' are not supported currently.",
"message": "Calls must be made with block_id of the latest or pending block. Other block_id are not supported.",
}


Expand Down
2 changes: 1 addition & 1 deletion test/rpc/test_rpc_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,5 +92,5 @@ def test_get_storage_at_raises_on_incorrect_block_id(deploy_info):

assert ex["error"] == {
"code": -1,
"message": "Calls with block_id != 'latest' are not supported currently.",
"message": "Calls must be made with block_id of the latest or pending block. Other block_id are not supported.",
}

0 comments on commit 715612b

Please sign in to comment.