diff --git a/docs/devnet_utils.rst b/docs/devnet_utils.rst new file mode 100644 index 000000000..2bb4c4df7 --- /dev/null +++ b/docs/devnet_utils.rst @@ -0,0 +1,6 @@ +Devnet Utils +============ + +.. toctree:: + + devnet_utils/mocking_interaction_with_l1 \ No newline at end of file diff --git a/docs/devnet_utils/mocking_interaction_with_l1.rst b/docs/devnet_utils/mocking_interaction_with_l1.rst new file mode 100644 index 000000000..00fbb7da8 --- /dev/null +++ b/docs/devnet_utils/mocking_interaction_with_l1.rst @@ -0,0 +1,65 @@ +Mocking interaction with L1 +=========================== + +Abstract +-------- + +In order to test interaction with L1 contracts, devnet client provides a way to mock the L1 interaction. +Before taking a look at the examples, please get familiar with the `devnet postman docs `_ and messaging mechanism: + +- `Writing messaging contracts `_ +- `Mechanism overview `_ +- `StarkGate example `_ + +L1 network setup +---------------- + +First of all you should deploy `messaging contract `_ +on ethereum network or load the existing one. + +.. codesnippet:: ../../starknet_py/tests/e2e/docs/devnet_utils/test_l1_integration.py + :language: python + :dedent: 4 + + +L2 -> L1 +-------- + +Deploying L2 interaction contract +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Interaction with L1 is done by sending a message using `send_message_to_l1_syscall` function. +So in order to test it, you need to deploy a contract that has this functionality. +Example contract: `l1_l2.cairo `_ + +.. codesnippet:: ../../starknet_py/tests/e2e/docs/devnet_utils/test_l1_integration.py + :language: python + :dedent: 4 + :start-after: docs: messaging-contract-start + :end-before: docs: messaging-contract-end + + +Consuming message +^^^^^^^^^^^^^^^^^ + +After deploying the contract, you need to flush the messages to the L1 network. +And then you can consume the message on the L1 network. + +.. codesnippet:: ../../starknet_py/tests/e2e/docs/devnet_utils/test_l1_integration.py + :language: python + :dedent: 4 + :start-after: docs: flush-1-start + :end-before: docs: flush-1-end + +L1 -> L2 +-------- + +Sending mock transactions from L1 to L2 does not require L1 node to be running. + +.. codesnippet:: ../../starknet_py/tests/e2e/docs/devnet_utils/test_l1_integration.py + :language: python + :dedent: 4 + :start-after: docs: send-l2-start + :end-before: docs: send-l2-end + + diff --git a/docs/index.rst b/docs/index.rst index 4d9a0ab45..1066b573c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -19,6 +19,7 @@ Starknet SDK for Python account_creation quickstart guide + devnet_utils api development migration_guide diff --git a/starknet_py/tests/e2e/client_devnet/fixtures/contracts.py b/starknet_py/tests/e2e/client_devnet/fixtures/contracts.py index ba0bcf976..4ac85b118 100644 --- a/starknet_py/tests/e2e/client_devnet/fixtures/contracts.py +++ b/starknet_py/tests/e2e/client_devnet/fixtures/contracts.py @@ -24,3 +24,21 @@ async def deploy_string_contract( contract_name="MyString", class_hash=f_string_contract_class_hash, ) + + +@pytest_asyncio.fixture(scope="package", name="l1_l2_contract_class_hash") +async def declare_l1_l2_contract(account) -> int: + contract = load_contract("l1_l2") + class_hash, _ = await declare_cairo1_contract( + account, contract["sierra"], contract["casm"] + ) + return class_hash + + +@pytest_asyncio.fixture(scope="package", name="l1_l2_contract") +async def deploy_l1_l2_contract(account, l1_l2_contract_class_hash) -> Contract: + return await deploy_v1_contract( + account=account, + contract_name="l1_l2", + class_hash=l1_l2_contract_class_hash, + ) diff --git a/starknet_py/tests/e2e/docs/devnet_utils/__init__.py b/starknet_py/tests/e2e/docs/devnet_utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/starknet_py/tests/e2e/docs/devnet_utils/test_l1_integration.py b/starknet_py/tests/e2e/docs/devnet_utils/test_l1_integration.py new file mode 100644 index 000000000..65ba38c1d --- /dev/null +++ b/starknet_py/tests/e2e/docs/devnet_utils/test_l1_integration.py @@ -0,0 +1,91 @@ +import pytest + +from starknet_py.hash.selector import get_selector_from_name +from starknet_py.net.client_models import ResourceBounds + + +@pytest.mark.skip(reason="Test require eth node running.") +@pytest.mark.asyncio +async def test_postman_load(devnet_client, l1_l2_contract, account): + # pylint: disable=import-outside-toplevel + # pylint: disable=unused-variable + + eth_account_address = 1390849295786071768276380950238675083608645509734 + + # docs: start + from starknet_py.devnet_utils.devnet_client import DevnetClient + + client = DevnetClient(node_url="http://127.0.0.1:5050") + + # docs: end + client: DevnetClient = devnet_client + # docs: start + # Deploying the messaging contract on ETH network + # e.g. anvil eth devnet https://github.com/foundry-rs/foundry/tree/master/crates/anvil + await client.postman_load(network_url="http://127.0.0.1:8545") + # docs: end + + # docs: messaging-contract-start + from starknet_py.contract import Contract + + # Address of your contract that is emitting messages + contract_address = "0x12345" + + # docs: messaging-contract-end + contract_address = l1_l2_contract.address + + # docs: messaging-contract-start + contract = await Contract.from_address(address=contract_address, provider=account) + + await contract.functions["increase_balance"].invoke_v3( + user=account.address, + amount=100, + l1_resource_bounds=ResourceBounds( + max_amount=50000, max_price_per_unit=int(1e12) + ), + ) + + # docs: messaging-contract-end + assert await contract.functions["get_balance"].call(user=account.address) == (100,) + + # docs: messaging-contract-start + # Invoking function that is emitting message + await contract.functions["withdraw"].invoke_v3( + user=account.address, + amount=100, + l1_address=eth_account_address, + l1_resource_bounds=ResourceBounds( + max_amount=50000, max_price_per_unit=int(1e12) + ), + ) + # docs: messaging-contract-end + assert await contract.functions["get_balance"].call(user=account.address) == (0,) + + # docs: flush-1-start + # Sending messages from L2 to L1. + flush_response = await client.postman_flush() + + message = flush_response.messages_to_l1[0] + + message_hash = await client.consume_message_from_l2( + from_address=message.from_address, + to_address=message.to_address, + payload=message.payload, + ) + # docs: flush-1-end + + # docs: send-l2-start + await client.send_message_to_l2( + l2_contract_address=contract_address, + entry_point_selector=get_selector_from_name("deposit"), + l1_contract_address="0xa000000000000000000000000000000000000001", + payload=[account.address, 100], + nonce="0x0", + paid_fee_on_l1="0xfffffffffff", + ) + + # Sending messages from L1 to L2. + flush_response = await client.postman_flush() + # docs: send-l2-end + + assert await contract.functions["get_balance"].call(user=account.address) == (100,) diff --git a/starknet_py/tests/e2e/mock/contracts_v2/src/l1_l2.cairo b/starknet_py/tests/e2e/mock/contracts_v2/src/l1_l2.cairo new file mode 100644 index 000000000..48c9c3724 --- /dev/null +++ b/starknet_py/tests/e2e/mock/contracts_v2/src/l1_l2.cairo @@ -0,0 +1,110 @@ +//! L1 L2 messaging demo contract. +//! Rewrite in Cairo 1 of the contract from previous Devnet version: +//! https://github.com/0xSpaceShard/starknet-devnet/blob/e477aa1bbe2348ba92af2a69c32d2eef2579d863/test/contracts/cairo/l1l2.cairo +//! +//! This contract does not use interface to keep the code as simple as possible. +//! + +#[starknet::contract] +mod l1_l2 { + const MESSAGE_WITHDRAW: felt252 = 0; + + #[storage] + struct Storage { + // Balances (users) -> (amount). + balances: LegacyMap, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + DepositFromL1: DepositFromL1, + } + + #[derive(Drop, starknet::Event)] + struct DepositFromL1 { + #[key] + user: felt252, + #[key] + amount: felt252, + } + + /// Gets the balance of the `user`. + #[external(v0)] + fn get_balance(self: @ContractState, user: felt252) -> felt252 { + self.balances.read(user) + } + + /// Increases the balance of the `user` for the `amount`. + #[external(v0)] + fn increase_balance(ref self: ContractState, user: felt252, amount: felt252) { + let balance = self.balances.read(user); + self.balances.write(user, balance + amount); + } + + /// Withdraws the `amount` for the `user` and sends a message to `l1_address` to + /// send the funds. + #[external(v0)] + fn withdraw(ref self: ContractState, user: felt252, amount: felt252, l1_address: felt252) { + assert(amount.is_non_zero(), 'Amount must be positive'); + + let balance = self.balances.read(user); + assert(balance.is_non_zero(), 'Balance is already 0'); + + // We need u256 to make comparisons. + let balance_u: u256 = balance.into(); + let amount_u: u256 = amount.into(); + assert(balance_u >= amount_u, 'Balance will be negative'); + + let new_balance = balance - amount; + + self.balances.write(user, new_balance); + + let payload = array![MESSAGE_WITHDRAW, user, amount,]; + + starknet::send_message_to_l1_syscall(l1_address, payload.span()).unwrap(); + } + + /// Withdraws the `amount` for the `user` and sends a message to `l1_address` to + /// send the funds. + #[external(v0)] + fn withdraw_from_lib( + ref self: ContractState, user: felt252, amount: felt252, l1_address: felt252, message_sender_class_hash: starknet::ClassHash, + ) { + assert(amount.is_non_zero(), 'Amount must be positive'); + + let balance = self.balances.read(user); + assert(balance.is_non_zero(), 'Balance is already 0'); + + // We need u256 to make comparisons. + let balance_u: u256 = balance.into(); + let amount_u: u256 = amount.into(); + assert(balance_u >= amount_u, 'Balance will be negative'); + + let new_balance = balance - amount; + + self.balances.write(user, new_balance); + + let calldata = array![user, amount, l1_address]; + + starknet::SyscallResultTrait::unwrap_syscall( + starknet::library_call_syscall( + message_sender_class_hash, + selector!("send_withdraw_message"), + calldata.span(), + ) + ); + } + + /// Deposits the `amount` for the `user`. Can only be called by the sequencer itself, + /// after having fetched some messages from the L1. + #[l1_handler] + fn deposit(ref self: ContractState, from_address: felt252, user: felt252, amount: felt252) { + // In a real case scenario, here we would assert from_address value + + let balance = self.balances.read(user); + self.balances.write(user, balance + amount); + + self.emit(DepositFromL1 { user, amount }); + } +} diff --git a/starknet_py/tests/e2e/mock/contracts_v2/src/lib.cairo b/starknet_py/tests/e2e/mock/contracts_v2/src/lib.cairo index 394bc11a4..10d919f5e 100644 --- a/starknet_py/tests/e2e/mock/contracts_v2/src/lib.cairo +++ b/starknet_py/tests/e2e/mock/contracts_v2/src/lib.cairo @@ -9,3 +9,4 @@ mod test_contract; mod test_enum; mod test_option; mod token_bridge; +mod l1_l2;