Skip to content

Commit 725685d

Browse files
feat: add ChainSimulatorSetStateStep for chain simulator state patching (#182)
* feat(providers): add set_address_state method Add set_address_state() to MyProxyNetworkProvider for posting hex-encoded key-value pairs to the chain simulator's address-specific set-state endpoint. * feat(steps): add ChainSimulatorSetStateStep Add new step to set specific storage key-value pairs for an address on the chain simulator. Includes hex validation and proper error handling for invalid input. * test(setup_steps): add comprehensive tests for ChainSimulatorSetStateStep Add 5 test cases covering: wrong network validation, successful execution, empty keys rejection, and invalid hex key/value rejection. Includes dedicated chain_simulator_scenario fixture and YAML example. * docs(steps): document ChainSimulatorSetStateStep Add user-facing documentation with description, YAML schema, and usage example for setting storage state on the chain simulator. * docs(changelog): add ChainSimulatorSetStateStep entry * Bump version: 3.1.1-dev2 → 3.1.1-dev3 --------- Co-authored-by: github-actions <41898282+github-actions@users.noreply.github.com>
1 parent de7044c commit 725685d

File tree

9 files changed

+148
-4
lines changed

9 files changed

+148
-4
lines changed

docs/source/dev_documentation/changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### Added
66

7+
- `ChainSimulatorSetStateStep` to set specific hex-encoded storage key-value pairs for an address on the chain simulator
78
- Explicit test for variadic counted values
89
- Dynamic batch size recovery: after a timeout reduces the batch size, subsequent successful requests with smaller payloads automatically double the batch size back toward the original (for both storage fetch and push operations)
910
- Tests verifying batch size resets between accounts

docs/source/user_documentation/steps.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -676,6 +676,20 @@ targets:
676676
- erd1y3296u7m2v5653pddey3p7l5zacqmsgqc7vsu3w74p9jm2qp3tqqz950yl # or direct bech32
677677
```
678678
679+
(chain_simulator_set_state_target)=
680+
### Chain Simulator Set State Step
681+
682+
Exclusive to the chain simulator.
683+
This step allows you to set specific storage key-value pairs for an address on the chain simulator. Keys and values must be hex-encoded. This is useful after cloning contracts from mainnet when some storage values (e.g. round numbers, epoch values) are incompatible with the simulator's timeline.
684+
685+
```yaml
686+
type: ChainSimulatorSetState
687+
address: "%my_contract.address"
688+
keys:
689+
"736166655f70726963655f63757272656e745f696e646578": "00000000"
690+
"6d795f6f746865725f6b6579": "01"
691+
```
692+
679693
(account_clone_target)=
680694
### Account Clone Step
681695

mxops/common/providers.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,16 @@ def set_state(self, states: list[dict]) -> GenericResponse:
423423
url = "simulator/set-state"
424424
return self.do_post_generic(url, states)
425425

426+
def set_address_state(self, address: str, keys: dict[str, str]) -> GenericResponse:
427+
"""Set specific storage key-value pairs for an address on the chain simulator.
428+
429+
:param address: bech32 address to set state for
430+
:param keys: hex-encoded key-value pairs to set
431+
:return: response from the chain simulator
432+
"""
433+
url = f"simulator/address/{address}/set-state"
434+
return self.do_post_generic(url, keys)
435+
426436
def set_state_overwrite(self, states: list[dict]) -> GenericResponse:
427437
url = "simulator/set-state-overwrite"
428438
return self.do_post_generic(url, states)

mxops/execution/steps/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from mxops.execution.steps.setup import (
1818
AccountCloneStep,
1919
ChainSimulatorFaucetStep,
20+
ChainSimulatorSetStateStep,
2021
GenerateWalletsStep,
2122
R3D4FaucetStep,
2223
)
@@ -45,6 +46,7 @@
4546
"AccountCloneStep",
4647
"AssertStep",
4748
"ChainSimulatorFaucetStep",
49+
"ChainSimulatorSetStateStep",
4850
"ContractCallStep",
4951
"ContractDeployStep",
5052
"ContractQueryStep",

mxops/execution/steps/setup.py

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
from mxops.enums import LogGroupEnum, NetworkEnum, parse_network_enum
3838
from mxops.execution import utils
3939
from mxops.execution.account import AccountsManager
40-
from mxops.smart_values import SmartInt, SmartPath, SmartValue
40+
from mxops.smart_values import SmartDict, SmartInt, SmartPath, SmartValue
4141
from mxops.smart_values.mx_sdk import SmartAddress, SmartAddresses
4242
from mxops.smart_values.native import SmartBool, SmartDatetime, SmartStr
4343
from mxops.execution.steps.base import Step
@@ -258,6 +258,60 @@ def _execute(self):
258258
egld_transfer_step.execute()
259259

260260

261+
@dataclass
262+
class ChainSimulatorSetStateStep(Step):
263+
"""
264+
Represents a step to set specific storage key-value pairs for an address
265+
on the chain simulator using hex-encoded keys and values.
266+
"""
267+
268+
address: SmartAddress
269+
keys: SmartDict
270+
ALLOWED_NETWORKS: ClassVar[set] = (NetworkEnum.CHAIN_SIMULATOR,)
271+
272+
@staticmethod
273+
def _validate_hex_keys(keys: dict[str, str]):
274+
"""Validate that all keys and values are valid hex strings."""
275+
if not keys:
276+
raise errors.InvalidSceneDefinition(
277+
"ChainSimulatorSetState requires at least one key-value pair"
278+
)
279+
for k, v in keys.items():
280+
try:
281+
bytes.fromhex(k)
282+
except ValueError as exc:
283+
raise errors.InvalidSceneDefinition(
284+
f"ChainSimulatorSetState key must be hex-encoded, got: '{k}'"
285+
) from exc
286+
try:
287+
bytes.fromhex(v)
288+
except ValueError as exc:
289+
raise errors.InvalidSceneDefinition(
290+
f"ChainSimulatorSetState value must be hex-encoded, got: '{v}'"
291+
) from exc
292+
293+
def _execute(self):
294+
"""
295+
Post hex-encoded key-value pairs to the chain simulator's
296+
address-specific set-state endpoint.
297+
"""
298+
logger = ScenarioData.get_scenario_logger(LogGroupEnum.EXEC)
299+
scenario_data = ScenarioData.get()
300+
if scenario_data.network not in self.ALLOWED_NETWORKS:
301+
raise errors.WrongNetworkForStep(
302+
scenario_data.network, self.ALLOWED_NETWORKS
303+
)
304+
proxy = MyProxyNetworkProvider()
305+
bech32 = self.address.get_evaluated_value().to_bech32()
306+
keys = self.keys.get_evaluated_value()
307+
self._validate_hex_keys(keys)
308+
logger.info(
309+
f"Setting {len(keys)} storage key(s) for {bech32} on chain simulator"
310+
)
311+
response = proxy.set_address_state(bech32, keys)
312+
logger.debug(f"set-state response: {response.to_dictionary()}")
313+
314+
261315
@dataclass
262316
class AccountCloneStep(Step):
263317
"""

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
55

66
[project]
77
name = "mxops"
8-
version = "3.1.1-dev2"
8+
version = "3.1.1-dev3"
99
authors = [
1010
{name="Etienne Wallet"},
1111
]

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[bumpversion]
2-
current_version = 3.1.1-dev2
2+
current_version = 3.1.1-dev3
33
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(-(?P<release>[^-0-9]+)(?P<build>\d+))?
44
serialize =
55
{major}.{minor}.{patch}-{release}{build}

tests/data/all_steps.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ steps:
6969
- bob
7070
amount: "={10**18}" # amount to send to each wallet
7171

72+
- type: ChainSimulatorSetState
73+
address: "%my_contract.address"
74+
keys:
75+
"736166655f70726963655f63757272656e745f696e646578": "00000000"
76+
7277
- type: GenerateWallets
7378
save_folder: path/to/wallets/folder # folder where to save the generated wallets
7479
wallets: # wallets to generate, can also just supply a number of wallets

tests/test_setup_steps.py

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
import os
22
from pathlib import Path
33
import shutil
4+
from unittest.mock import patch
45

56
from multiversx_sdk import Account
6-
from mxops.execution.steps import GenerateWalletsStep
7+
import pytest
8+
9+
from mxops import errors
10+
from mxops.enums import NetworkEnum
11+
from mxops.execution.steps import ChainSimulatorSetStateStep, GenerateWalletsStep
712

813

914
def test_generate_n_wallet_step():
@@ -100,3 +105,56 @@ def test_generate_n_keystore_wallet_step():
100105
# Cleanup
101106
shutil.rmtree("./tests/data/TEMP_UNIT_TEST")
102107
del os.environ["TEST_WALLET_PASSWORD"]
108+
109+
110+
MOCK_BECH32 = "erd1qqqqqqqqqqqqqpgqdmq43snzxutandvqefxgj89r6fh528v9dwnswvgq9t"
111+
MOCK_KEYS = {"736166655f70726963655f63757272656e745f696e646578": "00000000"}
112+
113+
114+
@pytest.fixture
115+
def chain_simulator_scenario(scenario_data, chain_simulator_network):
116+
"""Set both Config and ScenarioData to chain simulator network."""
117+
original = scenario_data.network
118+
scenario_data.network = NetworkEnum.CHAIN_SIMULATOR
119+
yield scenario_data
120+
scenario_data.network = original
121+
122+
123+
def test_chain_simulator_set_state_wrong_network(scenario_data):
124+
"""Network is LOCAL by default, step should reject it."""
125+
step = ChainSimulatorSetStateStep(address=MOCK_BECH32, keys=MOCK_KEYS)
126+
with pytest.raises(errors.WrongNetworkForStep):
127+
step.execute()
128+
129+
130+
def test_chain_simulator_set_state_success(chain_simulator_scenario):
131+
step = ChainSimulatorSetStateStep(address=MOCK_BECH32, keys=MOCK_KEYS)
132+
with patch(
133+
"mxops.execution.steps.setup.MyProxyNetworkProvider.set_address_state"
134+
) as mock_set:
135+
step.execute()
136+
mock_set.assert_called_once_with(MOCK_BECH32, MOCK_KEYS)
137+
138+
139+
def test_chain_simulator_set_state_empty_keys(chain_simulator_scenario):
140+
step = ChainSimulatorSetStateStep(address=MOCK_BECH32, keys={})
141+
with pytest.raises(errors.InvalidSceneDefinition):
142+
step.execute()
143+
144+
145+
def test_chain_simulator_set_state_invalid_hex_key(chain_simulator_scenario):
146+
step = ChainSimulatorSetStateStep(
147+
address=MOCK_BECH32, keys={"not_hex!": "00"}
148+
)
149+
with pytest.raises(errors.InvalidSceneDefinition, match="key must be hex-encoded"):
150+
step.execute()
151+
152+
153+
def test_chain_simulator_set_state_invalid_hex_value(chain_simulator_scenario):
154+
step = ChainSimulatorSetStateStep(
155+
address=MOCK_BECH32, keys={"00": "not_hex!"}
156+
)
157+
with pytest.raises(
158+
errors.InvalidSceneDefinition, match="value must be hex-encoded"
159+
):
160+
step.execute()

0 commit comments

Comments
 (0)