Skip to content

Commit e308872

Browse files
authored
Add interact_with_state closure cheatcode (#3526)
Towards #3331 commit-id:0b8b8c6b --- **Stack**: - #3539 - #3527 - #3526⚠️ *Part of a stack created by [spr](https://github.com/ejoffe/spr). Do not merge manually using the UI - doing so may have unexpected results.*
1 parent b1c6725 commit e308872

File tree

16 files changed

+589
-0
lines changed

16 files changed

+589
-0
lines changed

.github/workflows/ci.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,22 @@ jobs:
159159

160160
- run: cargo test --package forge --features scarb_since_2_10 sierra_gas
161161

162+
# TODO(#3212): Closures in Cairo are fully supported since version 2.11
163+
test-interact-with-state:
164+
name: Test interact with state
165+
runs-on: ubuntu-latest
166+
steps:
167+
- uses: actions/checkout@v4
168+
- uses: dtolnay/rust-toolchain@stable
169+
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6
170+
- uses: software-mansion/setup-scarb@v1
171+
with:
172+
scarb-version: "2.11.0"
173+
- uses: software-mansion/setup-universal-sierra-compiler@v1
174+
175+
- run: cargo test --release --package forge --features interact-with-state --test main integration::interact_with_state
176+
- run: cargo test --release --package forge --features interact-with-state --test main e2e::running::test_interact_with_state
177+
162178
test-forge-runner:
163179
name: Test Forge Runner
164180
runs-on: ubuntu-latest

crates/forge/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ no_scarb_installed = []
1313
debugging = []
1414
assert_non_exact_gas = ["test_utils/assert_non_exact_gas"]
1515
supports-panic-backtrace = []
16+
interact-with-state = []
1617

1718
[dependencies]
1819
anyhow.workspace = true
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[package]
2+
name = "contract_state"
3+
version = "0.1.0"
4+
edition = "2024_07"
5+
6+
[dependencies]
7+
starknet = "2.11.0"
8+
9+
[dev-dependencies]
10+
snforge_std = { path = "../../../../../snforge_std" }
11+
assert_macros = "2.11.0"
12+
13+
[[target.starknet-contract]]
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
#[starknet::interface]
2+
pub trait IHelloStarknetExtended<TContractState> {
3+
fn increase_balance(ref self: TContractState, amount: u256);
4+
fn get_balance(self: @TContractState) -> u256;
5+
fn get_caller_info(self: @TContractState, address: starknet::ContractAddress) -> u256;
6+
fn get_balance_at(self: @TContractState, index: u64) -> u256;
7+
}
8+
9+
10+
#[starknet::contract]
11+
pub mod HelloStarknetExtended {
12+
use starknet::storage::{
13+
Map, MutableVecTrait, StoragePathEntry, StoragePointerReadAccess, StoragePointerWriteAccess,
14+
Vec, VecTrait,
15+
};
16+
use starknet::{ContractAddress, get_caller_address};
17+
18+
#[derive(starknet::Store)]
19+
struct Owner {
20+
pub address: ContractAddress,
21+
pub name: felt252,
22+
}
23+
24+
#[storage]
25+
struct Storage {
26+
pub owner: Owner,
27+
pub balance: u256,
28+
pub balance_records: Vec<u256>,
29+
pub callers: Map<ContractAddress, u256>,
30+
}
31+
32+
#[constructor]
33+
fn constructor(ref self: ContractState, owner_name: felt252) {
34+
self
35+
._set_owner(
36+
starknet::get_execution_info().tx_info.account_contract_address, owner_name,
37+
);
38+
self.balance_records.push(0);
39+
}
40+
41+
#[abi(embed_v0)]
42+
impl HelloStarknetExtendedImpl of super::IHelloStarknetExtended<ContractState> {
43+
fn increase_balance(ref self: ContractState, amount: u256) {
44+
let caller = get_caller_address();
45+
let value_before = self.callers.entry(caller).read();
46+
47+
assert(amount != 0, 'Amount cannot be 0');
48+
49+
self.balance.write(self.balance.read() + amount);
50+
self.callers.entry(caller).write(value_before + amount);
51+
self.balance_records.push(self.balance.read());
52+
}
53+
54+
fn get_balance(self: @ContractState) -> u256 {
55+
self.balance.read()
56+
}
57+
58+
fn get_caller_info(self: @ContractState, address: ContractAddress) -> u256 {
59+
self.callers.entry(address).read()
60+
}
61+
62+
fn get_balance_at(self: @ContractState, index: u64) -> u256 {
63+
assert(index < self.balance_records.len(), 'Index out of range');
64+
self.balance_records.at(index).read()
65+
}
66+
}
67+
68+
#[generate_trait]
69+
pub impl InternalFunctions of InternalFunctionsTrait {
70+
fn _set_owner(ref self: ContractState, address: ContractAddress, name: felt252) {
71+
self.owner.address.write(address);
72+
self.owner.name.write(name);
73+
}
74+
}
75+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
pub mod balance;
2+
pub mod storage_node;
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
#[starknet::interface]
2+
pub trait IStorageNodeContract<TContractState> {
3+
fn get_description_at(self: @TContractState, index: u64) -> felt252;
4+
fn get_data_at(
5+
self: @TContractState, index: u64, address: starknet::ContractAddress, key: u16,
6+
) -> ByteArray;
7+
}
8+
9+
#[starknet::contract]
10+
pub mod StorageNodeContract {
11+
use starknet::ContractAddress;
12+
use starknet::storage::{StoragePointerReadAccess, StoragePathEntry, Map};
13+
14+
#[starknet::storage_node]
15+
pub struct RandomData {
16+
pub description: felt252,
17+
pub data: Map<(ContractAddress, u16), ByteArray>,
18+
}
19+
20+
#[storage]
21+
pub struct Storage {
22+
pub random_data: Map<u64, RandomData>,
23+
}
24+
25+
#[abi(embed_v0)]
26+
impl IStorageNodeContractImpl of super::IStorageNodeContract<ContractState> {
27+
fn get_description_at(self: @ContractState, index: u64) -> felt252 {
28+
self.random_data.entry(index).description.read()
29+
}
30+
31+
fn get_data_at(
32+
self: @ContractState, index: u64, address: ContractAddress, key: u16,
33+
) -> ByteArray {
34+
self.random_data.entry(index).data.entry((address, key)).read()
35+
}
36+
}
37+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#[starknet::interface]
2+
trait IMap<TMapState> {
3+
fn put(ref self: TMapState, key: felt252, value: felt252);
4+
fn get(self: @TMapState, key: felt252) -> felt252;
5+
}
6+
7+
#[starknet::contract]
8+
mod Map {
9+
use starknet::storage::{Map, StorageMapReadAccess, StoragePathEntry, StoragePointerWriteAccess};
10+
#[storage]
11+
pub struct Storage {
12+
pub storage: Map<felt252, felt252>,
13+
}
14+
15+
#[abi(embed_v0)]
16+
impl MapImpl of super::IMap<ContractState> {
17+
fn put(ref self: ContractState, key: felt252, value: felt252) {
18+
self.storage.entry(key).write(value);
19+
}
20+
21+
fn get(self: @ContractState, key: felt252) -> felt252 {
22+
self.storage.read(key)
23+
}
24+
}
25+
}
26+
use snforge_std::interact_with_state;
27+
use starknet::ContractAddress;
28+
use starknet::storage::StorageMapWriteAccess;
29+
30+
#[test]
31+
#[fork(url: "{{ NODE_RPC_URL }}", block_number: 900_000)]
32+
fn test_fork_contract() {
33+
let contract_address: ContractAddress =
34+
0x00cd8f9ab31324bb93251837e4efb4223ee195454f6304fcfcb277e277653008
35+
.try_into()
36+
.unwrap();
37+
let dispatcher = IMapDispatcher { contract_address };
38+
39+
assert(dispatcher.get(1) == 2, 'Wrong value');
40+
41+
interact_with_state(
42+
contract_address,
43+
|| {
44+
let mut state = Map::contract_state_for_testing();
45+
state.storage.write(1, 13579)
46+
},
47+
);
48+
49+
assert(dispatcher.get(1) == 13579, 'Wrong value');
50+
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
use contract_state::balance::HelloStarknetExtended::InternalFunctionsTrait;
2+
use contract_state::balance::{
3+
HelloStarknetExtended, IHelloStarknetExtendedDispatcher, IHelloStarknetExtendedDispatcherTrait,
4+
};
5+
use snforge_std::interact_with_state;
6+
use starknet::ContractAddress;
7+
use starknet::storage::{
8+
MutableVecTrait, StorageMapWriteAccess, StoragePointerReadAccess, StoragePointerWriteAccess,
9+
};
10+
use crate::utils::deploy_contract;
11+
12+
#[test]
13+
fn test_interact_with_state() {
14+
let contract_address = deploy_contract("HelloStarknetExtended", array!['Name']);
15+
let dispatcher = IHelloStarknetExtendedDispatcher { contract_address };
16+
17+
assert(dispatcher.get_balance() == 0, 'Wrong balance');
18+
19+
interact_with_state(
20+
contract_address,
21+
|| {
22+
let mut state = HelloStarknetExtended::contract_state_for_testing();
23+
state.balance.write(987);
24+
},
25+
);
26+
27+
assert(dispatcher.get_balance() == 987, 'Wrong balance');
28+
dispatcher.increase_balance(13);
29+
assert(dispatcher.get_balance() == 1000, 'Wrong balance');
30+
}
31+
32+
#[test]
33+
fn test_interact_with_state_return() {
34+
let contract_address = deploy_contract("HelloStarknetExtended", array!['Name']);
35+
let dispatcher = IHelloStarknetExtendedDispatcher { contract_address };
36+
37+
assert(dispatcher.get_balance() == 0, 'Wrong balance');
38+
39+
let res = interact_with_state(
40+
contract_address,
41+
|| -> u256 {
42+
let mut state = HelloStarknetExtended::contract_state_for_testing();
43+
state.balance.write(111);
44+
state.balance.read()
45+
},
46+
);
47+
48+
assert(res == 111, 'Wrong balance');
49+
}
50+
51+
#[test]
52+
fn test_interact_with_initialized_state() {
53+
let contract_address = deploy_contract("HelloStarknetExtended", array!['Name']);
54+
let dispatcher = IHelloStarknetExtendedDispatcher { contract_address };
55+
56+
dispatcher.increase_balance(199);
57+
58+
interact_with_state(
59+
contract_address,
60+
|| {
61+
let mut state = HelloStarknetExtended::contract_state_for_testing();
62+
assert(state.balance.read() == 199, 'Wrong balance');
63+
state.balance.write(1);
64+
},
65+
);
66+
67+
assert(dispatcher.get_balance() == 1, 'Wrong balance');
68+
}
69+
70+
#[test]
71+
fn test_interact_with_state_vec() {
72+
let contract_address = deploy_contract("HelloStarknetExtended", array!['Name']);
73+
let dispatcher = IHelloStarknetExtendedDispatcher { contract_address };
74+
75+
dispatcher.increase_balance(1);
76+
dispatcher.increase_balance(1);
77+
dispatcher.increase_balance(1);
78+
79+
interact_with_state(
80+
contract_address,
81+
|| {
82+
let mut state = HelloStarknetExtended::contract_state_for_testing();
83+
assert(state.balance_records.len() == 4, 'Wrong length');
84+
state.balance_records.push(10);
85+
},
86+
);
87+
88+
assert(dispatcher.get_balance_at(0) == 0, 'Wrong balance');
89+
assert(dispatcher.get_balance_at(2) == 2, 'Wrong balance');
90+
assert(dispatcher.get_balance_at(4) == 10, 'Wrong balance');
91+
}
92+
93+
#[test]
94+
fn test_interact_with_state_map() {
95+
let contract_address = deploy_contract("HelloStarknetExtended", array!['Name']);
96+
let dispatcher = IHelloStarknetExtendedDispatcher { contract_address };
97+
98+
dispatcher.increase_balance(1);
99+
100+
interact_with_state(
101+
contract_address,
102+
|| {
103+
let mut state = HelloStarknetExtended::contract_state_for_testing();
104+
state.callers.write(0x123.try_into().unwrap(), 1000);
105+
state.callers.write(0x321.try_into().unwrap(), 2000);
106+
},
107+
);
108+
109+
assert(
110+
dispatcher.get_caller_info(0x123.try_into().unwrap()) == 1000,
111+
'Wrong data for address 0x123',
112+
);
113+
assert(
114+
dispatcher.get_caller_info(0x321.try_into().unwrap()) == 2000,
115+
'Wrong data for address 0x321',
116+
);
117+
assert(
118+
dispatcher.get_caller_info(0x12345.try_into().unwrap()) == 0,
119+
'Wrong data for address 0x12345',
120+
);
121+
}
122+
123+
#[test]
124+
fn test_interact_with_state_internal_function() {
125+
let contract_address = deploy_contract("HelloStarknetExtended", array!['Name']);
126+
127+
let get_owner =
128+
|| -> (
129+
ContractAddress, felt252,
130+
) {
131+
interact_with_state(
132+
contract_address,
133+
|| -> (
134+
ContractAddress, felt252,
135+
) {
136+
let mut state = HelloStarknetExtended::contract_state_for_testing();
137+
(state.owner.address.read(), state.owner.name.read())
138+
},
139+
)
140+
};
141+
let (owner_address, owner_name) = get_owner();
142+
assert(owner_address == 0.try_into().unwrap(), 'Incorrect owner address');
143+
assert(owner_name == 'Name', 'Incorrect owner name');
144+
145+
interact_with_state(
146+
contract_address,
147+
|| {
148+
let mut state = HelloStarknetExtended::contract_state_for_testing();
149+
state._set_owner(0x777.try_into().unwrap(), 'New name');
150+
},
151+
);
152+
let (owner_address, owner_name) = get_owner();
153+
154+
assert(owner_address == 0x777.try_into().unwrap(), 'Incorrect owner address');
155+
assert(owner_name == 'New name', 'Incorrect owner name');
156+
}

0 commit comments

Comments
 (0)