Skip to content

Commit 31347a2

Browse files
authored
Improve DevnetClient docs (#1415)
* Add L1-L2 communication docs guide * Restructure folders * Enhance docs, fix typos
1 parent 1a8814c commit 31347a2

File tree

8 files changed

+292
-0
lines changed

8 files changed

+292
-0
lines changed

docs/devnet_utils.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Devnet Utils
2+
============
3+
4+
.. toctree::
5+
6+
devnet_utils/mocking_interaction_with_l1
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
Mocking interaction with L1
2+
===========================
3+
4+
Abstract
5+
--------
6+
7+
In order to test interaction with L1 contracts, devnet client provides a way to mock the L1 interaction.
8+
Before taking a look at the examples, please get familiar with the `devnet postman docs <https://0xspaceshard.github.io/starknet-devnet-rs/docs/postman>`_ and messaging mechanism:
9+
10+
- `Writing messaging contracts <https://book.cairo-lang.org/ch16-04-L1-L2-messaging.html>`_
11+
- `Mechanism overview <https://docs.starknet.io/architecture-and-concepts/network-architecture/messaging-mechanism/>`_
12+
- `StarkGate example <https://docs.starknet.io/architecture-and-concepts/network-architecture/messaging-mechanism/>`_
13+
14+
L1 network setup
15+
----------------
16+
17+
First of all you should deploy `messaging contract <https://github.com/0xSpaceShard/starknet-devnet-rs/blob/138120b355c44ae60269167b326d1a267f7af0a8/contracts/l1-l2-messaging/solidity/src/MockStarknetMessaging.sol>`_
18+
on ethereum network or load the existing one.
19+
20+
.. codesnippet:: ../../starknet_py/tests/e2e/docs/devnet_utils/test_l1_integration.py
21+
:language: python
22+
:dedent: 4
23+
24+
25+
L2 -> L1
26+
--------
27+
28+
Deploying L2 interaction contract
29+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
30+
31+
Interaction with L1 is done by sending a message using `send_message_to_l1_syscall` function.
32+
So in order to test it, you need to deploy a contract that has this functionality.
33+
Example contract: `l1_l2.cairo <https://github.com/0xSpaceShard/starknet-devnet-js/blob/5069ec3397f31a408d3df2734ae40d93b42a0f7f/test/data/l1_l2.cairo>`_
34+
35+
.. codesnippet:: ../../starknet_py/tests/e2e/docs/devnet_utils/test_l1_integration.py
36+
:language: python
37+
:dedent: 4
38+
:start-after: docs: messaging-contract-start
39+
:end-before: docs: messaging-contract-end
40+
41+
42+
Consuming message
43+
^^^^^^^^^^^^^^^^^
44+
45+
After deploying the contract, you need to flush the messages to the L1 network.
46+
And then you can consume the message on the L1 network.
47+
48+
.. codesnippet:: ../../starknet_py/tests/e2e/docs/devnet_utils/test_l1_integration.py
49+
:language: python
50+
:dedent: 4
51+
:start-after: docs: flush-1-start
52+
:end-before: docs: flush-1-end
53+
54+
L1 -> L2
55+
--------
56+
57+
Sending mock transactions from L1 to L2 does not require L1 node to be running.
58+
59+
.. codesnippet:: ../../starknet_py/tests/e2e/docs/devnet_utils/test_l1_integration.py
60+
:language: python
61+
:dedent: 4
62+
:start-after: docs: send-l2-start
63+
:end-before: docs: send-l2-end
64+
65+

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Starknet SDK for Python
1919
account_creation
2020
quickstart
2121
guide
22+
devnet_utils
2223
api
2324
development
2425
migration_guide

starknet_py/tests/e2e/client_devnet/fixtures/contracts.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,21 @@ async def deploy_string_contract(
2424
contract_name="MyString",
2525
class_hash=f_string_contract_class_hash,
2626
)
27+
28+
29+
@pytest_asyncio.fixture(scope="package", name="l1_l2_contract_class_hash")
30+
async def declare_l1_l2_contract(account) -> int:
31+
contract = load_contract("l1_l2")
32+
class_hash, _ = await declare_cairo1_contract(
33+
account, contract["sierra"], contract["casm"]
34+
)
35+
return class_hash
36+
37+
38+
@pytest_asyncio.fixture(scope="package", name="l1_l2_contract")
39+
async def deploy_l1_l2_contract(account, l1_l2_contract_class_hash) -> Contract:
40+
return await deploy_v1_contract(
41+
account=account,
42+
contract_name="l1_l2",
43+
class_hash=l1_l2_contract_class_hash,
44+
)

starknet_py/tests/e2e/docs/devnet_utils/__init__.py

Whitespace-only changes.
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import pytest
2+
3+
from starknet_py.hash.selector import get_selector_from_name
4+
from starknet_py.net.client_models import ResourceBounds
5+
6+
7+
@pytest.mark.skip(reason="Test require eth node running.")
8+
@pytest.mark.asyncio
9+
async def test_postman_load(devnet_client, l1_l2_contract, account):
10+
# pylint: disable=import-outside-toplevel
11+
# pylint: disable=unused-variable
12+
13+
eth_account_address = 1390849295786071768276380950238675083608645509734
14+
15+
# docs: start
16+
from starknet_py.devnet_utils.devnet_client import DevnetClient
17+
18+
client = DevnetClient(node_url="http://127.0.0.1:5050")
19+
20+
# docs: end
21+
client: DevnetClient = devnet_client
22+
# docs: start
23+
# Deploying the messaging contract on ETH network
24+
# e.g. anvil eth devnet https://github.com/foundry-rs/foundry/tree/master/crates/anvil
25+
await client.postman_load(network_url="http://127.0.0.1:8545")
26+
# docs: end
27+
28+
# docs: messaging-contract-start
29+
from starknet_py.contract import Contract
30+
31+
# Address of your contract that is emitting messages
32+
contract_address = "0x12345"
33+
34+
# docs: messaging-contract-end
35+
contract_address = l1_l2_contract.address
36+
37+
# docs: messaging-contract-start
38+
contract = await Contract.from_address(address=contract_address, provider=account)
39+
40+
await contract.functions["increase_balance"].invoke_v3(
41+
user=account.address,
42+
amount=100,
43+
l1_resource_bounds=ResourceBounds(
44+
max_amount=50000, max_price_per_unit=int(1e12)
45+
),
46+
)
47+
48+
# docs: messaging-contract-end
49+
assert await contract.functions["get_balance"].call(user=account.address) == (100,)
50+
51+
# docs: messaging-contract-start
52+
# Invoking function that is emitting message
53+
await contract.functions["withdraw"].invoke_v3(
54+
user=account.address,
55+
amount=100,
56+
l1_address=eth_account_address,
57+
l1_resource_bounds=ResourceBounds(
58+
max_amount=50000, max_price_per_unit=int(1e12)
59+
),
60+
)
61+
# docs: messaging-contract-end
62+
assert await contract.functions["get_balance"].call(user=account.address) == (0,)
63+
64+
# docs: flush-1-start
65+
# Sending messages from L2 to L1.
66+
flush_response = await client.postman_flush()
67+
68+
message = flush_response.messages_to_l1[0]
69+
70+
message_hash = await client.consume_message_from_l2(
71+
from_address=message.from_address,
72+
to_address=message.to_address,
73+
payload=message.payload,
74+
)
75+
# docs: flush-1-end
76+
77+
# docs: send-l2-start
78+
await client.send_message_to_l2(
79+
l2_contract_address=contract_address,
80+
entry_point_selector=get_selector_from_name("deposit"),
81+
l1_contract_address="0xa000000000000000000000000000000000000001",
82+
payload=[account.address, 100],
83+
nonce="0x0",
84+
paid_fee_on_l1="0xfffffffffff",
85+
)
86+
87+
# Sending messages from L1 to L2.
88+
flush_response = await client.postman_flush()
89+
# docs: send-l2-end
90+
91+
assert await contract.functions["get_balance"].call(user=account.address) == (100,)
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
//! L1 L2 messaging demo contract.
2+
//! Rewrite in Cairo 1 of the contract from previous Devnet version:
3+
//! https://github.com/0xSpaceShard/starknet-devnet/blob/e477aa1bbe2348ba92af2a69c32d2eef2579d863/test/contracts/cairo/l1l2.cairo
4+
//!
5+
//! This contract does not use interface to keep the code as simple as possible.
6+
//!
7+
8+
#[starknet::contract]
9+
mod l1_l2 {
10+
const MESSAGE_WITHDRAW: felt252 = 0;
11+
12+
#[storage]
13+
struct Storage {
14+
// Balances (users) -> (amount).
15+
balances: LegacyMap<felt252, felt252>,
16+
}
17+
18+
#[event]
19+
#[derive(Drop, starknet::Event)]
20+
enum Event {
21+
DepositFromL1: DepositFromL1,
22+
}
23+
24+
#[derive(Drop, starknet::Event)]
25+
struct DepositFromL1 {
26+
#[key]
27+
user: felt252,
28+
#[key]
29+
amount: felt252,
30+
}
31+
32+
/// Gets the balance of the `user`.
33+
#[external(v0)]
34+
fn get_balance(self: @ContractState, user: felt252) -> felt252 {
35+
self.balances.read(user)
36+
}
37+
38+
/// Increases the balance of the `user` for the `amount`.
39+
#[external(v0)]
40+
fn increase_balance(ref self: ContractState, user: felt252, amount: felt252) {
41+
let balance = self.balances.read(user);
42+
self.balances.write(user, balance + amount);
43+
}
44+
45+
/// Withdraws the `amount` for the `user` and sends a message to `l1_address` to
46+
/// send the funds.
47+
#[external(v0)]
48+
fn withdraw(ref self: ContractState, user: felt252, amount: felt252, l1_address: felt252) {
49+
assert(amount.is_non_zero(), 'Amount must be positive');
50+
51+
let balance = self.balances.read(user);
52+
assert(balance.is_non_zero(), 'Balance is already 0');
53+
54+
// We need u256 to make comparisons.
55+
let balance_u: u256 = balance.into();
56+
let amount_u: u256 = amount.into();
57+
assert(balance_u >= amount_u, 'Balance will be negative');
58+
59+
let new_balance = balance - amount;
60+
61+
self.balances.write(user, new_balance);
62+
63+
let payload = array![MESSAGE_WITHDRAW, user, amount,];
64+
65+
starknet::send_message_to_l1_syscall(l1_address, payload.span()).unwrap();
66+
}
67+
68+
/// Withdraws the `amount` for the `user` and sends a message to `l1_address` to
69+
/// send the funds.
70+
#[external(v0)]
71+
fn withdraw_from_lib(
72+
ref self: ContractState, user: felt252, amount: felt252, l1_address: felt252, message_sender_class_hash: starknet::ClassHash,
73+
) {
74+
assert(amount.is_non_zero(), 'Amount must be positive');
75+
76+
let balance = self.balances.read(user);
77+
assert(balance.is_non_zero(), 'Balance is already 0');
78+
79+
// We need u256 to make comparisons.
80+
let balance_u: u256 = balance.into();
81+
let amount_u: u256 = amount.into();
82+
assert(balance_u >= amount_u, 'Balance will be negative');
83+
84+
let new_balance = balance - amount;
85+
86+
self.balances.write(user, new_balance);
87+
88+
let calldata = array![user, amount, l1_address];
89+
90+
starknet::SyscallResultTrait::unwrap_syscall(
91+
starknet::library_call_syscall(
92+
message_sender_class_hash,
93+
selector!("send_withdraw_message"),
94+
calldata.span(),
95+
)
96+
);
97+
}
98+
99+
/// Deposits the `amount` for the `user`. Can only be called by the sequencer itself,
100+
/// after having fetched some messages from the L1.
101+
#[l1_handler]
102+
fn deposit(ref self: ContractState, from_address: felt252, user: felt252, amount: felt252) {
103+
// In a real case scenario, here we would assert from_address value
104+
105+
let balance = self.balances.read(user);
106+
self.balances.write(user, balance + amount);
107+
108+
self.emit(DepositFromL1 { user, amount });
109+
}
110+
}

starknet_py/tests/e2e/mock/contracts_v2/src/lib.cairo

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ mod test_contract;
99
mod test_enum;
1010
mod test_option;
1111
mod token_bridge;
12+
mod l1_l2;

0 commit comments

Comments
 (0)