genlayer-test 0.4.1__py3-none-any.whl → 0.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. {genlayer_test-0.4.1.dist-info → genlayer_test-0.5.0.dist-info}/METADATA +257 -24
  2. genlayer_test-0.5.0.dist-info/RECORD +76 -0
  3. gltest/__init__.py +7 -6
  4. gltest/{glchain/client.py → clients.py} +1 -1
  5. gltest/contracts/__init__.py +4 -0
  6. gltest/contracts/contract.py +205 -0
  7. gltest/{glchain/contract.py → contracts/contract_factory.py} +47 -144
  8. gltest/contracts/contract_functions.py +62 -0
  9. gltest/contracts/method_stats.py +163 -0
  10. gltest/contracts/stats_collector.py +259 -0
  11. gltest/contracts/utils.py +12 -0
  12. gltest/fixtures.py +2 -6
  13. gltest/helpers/take_snapshot.py +1 -1
  14. gltest/types.py +1 -0
  15. gltest_cli/config/constants.py +2 -0
  16. gltest_cli/config/plugin.py +121 -49
  17. gltest_cli/config/pytest_context.py +9 -0
  18. gltest_cli/config/types.py +73 -8
  19. gltest_cli/config/user.py +71 -28
  20. gltest_cli/logging.py +3 -2
  21. tests/examples/contracts/football_prediction_market.py +1 -1
  22. tests/examples/tests/test_football_prediction_market.py +2 -2
  23. tests/examples/tests/test_intelligent_oracle_factory.py +8 -24
  24. tests/examples/tests/test_llm_erc20.py +5 -5
  25. tests/examples/tests/test_llm_erc20_analyze.py +50 -0
  26. tests/examples/tests/test_log_indexer.py +23 -11
  27. tests/examples/tests/test_multi_file_contract.py +2 -2
  28. tests/examples/tests/test_multi_file_contract_legacy.py +2 -2
  29. tests/examples/tests/test_multi_read_erc20.py +14 -12
  30. tests/examples/tests/test_multi_tenant_storage.py +11 -7
  31. tests/examples/tests/test_read_erc20.py +1 -1
  32. tests/examples/tests/test_storage.py +4 -4
  33. tests/examples/tests/test_storage_legacy.py +5 -3
  34. tests/examples/tests/test_user_storage.py +20 -10
  35. tests/examples/tests/test_wizard_of_coin.py +1 -1
  36. tests/gltest_cli/config/test_config_integration.py +432 -0
  37. tests/gltest_cli/config/test_general_config.py +406 -0
  38. tests/gltest_cli/config/test_plugin.py +164 -1
  39. tests/gltest_cli/config/test_user.py +61 -1
  40. genlayer_test-0.4.1.dist-info/RECORD +0 -67
  41. gltest/glchain/__init__.py +0 -16
  42. {genlayer_test-0.4.1.dist-info → genlayer_test-0.5.0.dist-info}/WHEEL +0 -0
  43. {genlayer_test-0.4.1.dist-info → genlayer_test-0.5.0.dist-info}/entry_points.txt +0 -0
  44. {genlayer_test-0.4.1.dist-info → genlayer_test-0.5.0.dist-info}/licenses/LICENSE +0 -0
  45. {genlayer_test-0.4.1.dist-info → genlayer_test-0.5.0.dist-info}/top_level.txt +0 -0
  46. /gltest/{glchain/account.py → accounts.py} +0 -0
@@ -0,0 +1,259 @@
1
+ """
2
+ Stats collector module for contract method analysis.
3
+
4
+ This module contains classes and functions to collect and analyze statistics
5
+ from contract method executions, simplifying the analyze_method implementation.
6
+ """
7
+
8
+ import time
9
+ import json
10
+ import hashlib
11
+ from datetime import datetime, timezone
12
+ from typing import List, Dict, Any, Optional
13
+ from dataclasses import dataclass
14
+
15
+ from gltest.clients import get_gl_client
16
+ from gltest.types import CalldataEncodable
17
+ from .method_stats import StateGroup, FailedRun, MethodStatsDetailed, MethodStatsSummary
18
+ from gltest_cli.config.general import get_general_config
19
+ from gltest_cli.config.pytest_context import get_current_test_nodeid
20
+ from .utils import safe_filename
21
+
22
+
23
+ @dataclass
24
+ class SimulationConfig:
25
+ """Configuration for simulation runs."""
26
+
27
+ provider: str
28
+ model: str
29
+ config: Optional[Dict[str, Any]] = None
30
+ plugin: Optional[str] = None
31
+ plugin_config: Optional[Dict[str, Any]] = None
32
+
33
+
34
+ @dataclass
35
+ class SimulationResults:
36
+ """Results from simulation runs."""
37
+
38
+ sim_results: List[Dict[str, Any]]
39
+ failed_runs: List[FailedRun]
40
+ server_errors: int
41
+ execution_time: float
42
+ timestamp: str
43
+
44
+
45
+ class StatsCollector:
46
+ """Collects and analyzes statistics for contract method executions."""
47
+
48
+ def __init__(
49
+ self,
50
+ contract_address: str,
51
+ method_name: str,
52
+ account: Any,
53
+ args: Optional[List[CalldataEncodable]] = None,
54
+ ):
55
+ self.contract_address = contract_address
56
+ self.method_name = method_name
57
+ self.account = account
58
+ self.args = args or []
59
+ self.client = get_gl_client()
60
+
61
+ def run_simulations(
62
+ self, sim_config: SimulationConfig, runs: int
63
+ ) -> SimulationResults:
64
+ """Execute multiple simulation runs and collect results."""
65
+ start_time = time.time()
66
+ timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
67
+
68
+ sim_results = []
69
+ failed_runs_list = []
70
+ server_errors = 0
71
+
72
+ for run_idx in range(runs):
73
+ try:
74
+ sim_result = self._execute_single_simulation(sim_config)
75
+ sim_results.append(sim_result)
76
+
77
+ if sim_result.get("execution_result") != "SUCCESS":
78
+ failed_runs_list.append(
79
+ self._create_failed_run(run_idx, sim_result)
80
+ )
81
+ except Exception as e:
82
+ server_errors += 1
83
+ failed_runs_list.append(
84
+ FailedRun(
85
+ run=run_idx,
86
+ error=str(e),
87
+ error_type="server",
88
+ genvm_result=None,
89
+ )
90
+ )
91
+
92
+ execution_time = time.time() - start_time
93
+
94
+ return SimulationResults(
95
+ sim_results=sim_results,
96
+ failed_runs=failed_runs_list,
97
+ server_errors=server_errors,
98
+ execution_time=execution_time,
99
+ timestamp=timestamp,
100
+ )
101
+
102
+ def _execute_single_simulation(
103
+ self, sim_config: SimulationConfig
104
+ ) -> Dict[str, Any]:
105
+ """Execute a single simulation."""
106
+ config_dict = {
107
+ "provider": sim_config.provider,
108
+ "model": sim_config.model,
109
+ }
110
+
111
+ if (
112
+ sim_config.config is not None
113
+ and sim_config.plugin is not None
114
+ and sim_config.plugin_config is not None
115
+ ):
116
+ config_dict["config"] = sim_config.config
117
+ config_dict["plugin"] = sim_config.plugin
118
+ config_dict["plugin_config"] = sim_config.plugin_config
119
+
120
+ return self.client.simulate_write_contract(
121
+ address=self.contract_address,
122
+ function_name=self.method_name,
123
+ account=self.account,
124
+ args=self.args,
125
+ sim_config=config_dict,
126
+ )
127
+
128
+ def _create_failed_run(self, run_idx: int, sim_result: Dict[str, Any]) -> FailedRun:
129
+ """Create a FailedRun object from a failed simulation result."""
130
+ return FailedRun(
131
+ run=run_idx,
132
+ error=sim_result.get("error", "unknown error"),
133
+ error_type="simulation",
134
+ genvm_result={
135
+ "stderr": sim_result.get("genvm_result", {}).get("stderr", ""),
136
+ "stdout": sim_result.get("genvm_result", {}).get("stdout", ""),
137
+ },
138
+ )
139
+
140
+ def analyze_results(
141
+ self,
142
+ sim_results: SimulationResults,
143
+ runs: int,
144
+ sim_config: SimulationConfig,
145
+ ) -> MethodStatsSummary:
146
+ """Analyze simulation results and generate statistics."""
147
+ state_groups = self._analyze_states(sim_results.sim_results)
148
+
149
+ executed_runs = runs - sim_results.server_errors
150
+ successful_runs = sum(
151
+ 1
152
+ for sim_receipt in sim_results.sim_results
153
+ if sim_receipt.get("execution_result") == "SUCCESS"
154
+ )
155
+
156
+ most_common_count = max((group.count for group in state_groups), default=0)
157
+ reliability_score = (
158
+ (most_common_count / executed_runs) if executed_runs > 0 else 0.0
159
+ )
160
+
161
+ # Save detailed stats
162
+ detailed_stats = self._create_detailed_stats(
163
+ sim_results=sim_results,
164
+ state_groups=state_groups,
165
+ runs=runs,
166
+ executed_runs=executed_runs,
167
+ successful_runs=successful_runs,
168
+ most_common_count=most_common_count,
169
+ reliability_score=reliability_score,
170
+ sim_config=sim_config,
171
+ )
172
+ self._save_detailed_stats(detailed_stats)
173
+
174
+ # Return summary
175
+ return MethodStatsSummary(
176
+ method=self.method_name,
177
+ args=self.args,
178
+ total_runs=runs,
179
+ server_error_runs=sim_results.server_errors,
180
+ executed_runs=executed_runs,
181
+ failed_runs=len(sim_results.failed_runs),
182
+ successful_runs=successful_runs,
183
+ unique_states=len(state_groups),
184
+ most_common_state_count=most_common_count,
185
+ reliability_score=reliability_score,
186
+ execution_time=sim_results.execution_time,
187
+ provider=sim_config.provider,
188
+ model=sim_config.model,
189
+ )
190
+
191
+ def _analyze_states(self, sim_results: List[Dict[str, Any]]) -> List[StateGroup]:
192
+ """Analyze contract states from simulation results."""
193
+ state_counts = {}
194
+ state_to_hash_str = {}
195
+
196
+ for sim_receipt in sim_results:
197
+ contract_state = sim_receipt.get("contract_state", {})
198
+ state_json = json.dumps(contract_state, sort_keys=True)
199
+ state_hash = hashlib.sha256(state_json.encode()).hexdigest()
200
+ state_hash_str = f"0x{state_hash}"
201
+ state_to_hash_str[state_hash] = state_hash_str
202
+ state_counts[state_hash] = state_counts.get(state_hash, 0) + 1
203
+
204
+ return [
205
+ StateGroup(count=count, state_hash=state_to_hash_str[state_hash])
206
+ for state_hash, count in sorted(
207
+ state_counts.items(), key=lambda x: x[1], reverse=True
208
+ )
209
+ ]
210
+
211
+ def _create_detailed_stats(
212
+ self,
213
+ sim_results: SimulationResults,
214
+ state_groups: List[StateGroup],
215
+ runs: int,
216
+ executed_runs: int,
217
+ successful_runs: int,
218
+ most_common_count: int,
219
+ reliability_score: float,
220
+ sim_config: SimulationConfig,
221
+ ) -> MethodStatsDetailed:
222
+ """Create detailed statistics object."""
223
+ configuration = {
224
+ "runs": runs,
225
+ "provider": sim_config.provider,
226
+ "model": sim_config.model,
227
+ "config": sim_config.config,
228
+ "plugin": sim_config.plugin,
229
+ "plugin_config": sim_config.plugin_config,
230
+ }
231
+
232
+ return MethodStatsDetailed(
233
+ method=self.method_name,
234
+ params=self.args,
235
+ timestamp=sim_results.timestamp,
236
+ configuration=configuration,
237
+ execution_time=sim_results.execution_time,
238
+ executed_runs=executed_runs,
239
+ failed_runs=len(sim_results.failed_runs),
240
+ successful_runs=successful_runs,
241
+ server_error_runs=sim_results.server_errors,
242
+ unique_states=len(state_groups),
243
+ most_common_state_count=most_common_count,
244
+ reliability_score=reliability_score,
245
+ state_groups=state_groups,
246
+ failed_runs_results=sim_results.failed_runs,
247
+ sim_results=sim_results.sim_results,
248
+ )
249
+
250
+ def _save_detailed_stats(self, detailed_stats: MethodStatsDetailed) -> None:
251
+ """Save detailed statistics to the configured directory."""
252
+ general_config = get_general_config()
253
+ current_nodeid = get_current_test_nodeid()
254
+ if current_nodeid is None:
255
+ safe_name = "no_test"
256
+ else:
257
+ safe_name = safe_filename(current_nodeid)
258
+ stats_dir = general_config.get_analysis_dir() / safe_name
259
+ detailed_stats.save_to_directory(stats_dir)
@@ -0,0 +1,12 @@
1
+ def safe_filename(filename: str) -> str:
2
+ """
3
+ Replace problematic characters in filename.
4
+ """
5
+ return (
6
+ filename.replace("/", "_")
7
+ .replace("\\", "_")
8
+ .replace(":", "_")
9
+ .replace("-", "_")
10
+ .replace(" ", "_")
11
+ .replace(".py", "")
12
+ )
gltest/fixtures.py CHANGED
@@ -4,12 +4,8 @@ These fixtures can be imported and used in test files.
4
4
  """
5
5
 
6
6
  import pytest
7
- from gltest.glchain import (
8
- get_gl_client,
9
- get_accounts,
10
- get_default_account,
11
- get_gl_provider,
12
- )
7
+ from gltest.clients import get_gl_client, get_gl_provider
8
+ from gltest.accounts import get_accounts, get_default_account
13
9
  from gltest_cli.config.general import get_general_config
14
10
 
15
11
 
@@ -1,4 +1,4 @@
1
- from gltest.glchain import get_gl_provider
1
+ from gltest.clients import get_gl_provider
2
2
  from dataclasses import dataclass
3
3
  from typing import Callable
4
4
  from gltest.exceptions import HelperError, InvalidSnapshotError
gltest/types.py CHANGED
@@ -4,4 +4,5 @@ from genlayer_py.types import (
4
4
  GenLayerTransaction,
5
5
  TransactionStatus,
6
6
  CalldataEncodable,
7
+ TransactionHashVariant,
7
8
  )
@@ -4,7 +4,9 @@ from pathlib import Path
4
4
 
5
5
  GLTEST_CONFIG_FILE = "gltest.config.yaml"
6
6
  DEFAULT_NETWORK = "localnet"
7
+ PRECONFIGURED_NETWORKS = ["localnet", "studionet", "testnet_asimov"]
7
8
  DEFAULT_RPC_URL = SIMULATOR_JSON_RPC_URL
8
9
  DEFAULT_ENVIRONMENT = ".env"
9
10
  DEFAULT_CONTRACTS_DIR = Path("contracts")
11
+ DEFAULT_ARTIFACTS_DIR = Path("artifacts")
10
12
  DEFAULT_NETWORK_ID = 61999
@@ -1,4 +1,6 @@
1
+ import pytest
1
2
  from pathlib import Path
3
+ import shutil
2
4
  from gltest_cli.logging import logger
3
5
  from gltest_cli.config.user import (
4
6
  user_config_exists,
@@ -9,6 +11,7 @@ from gltest_cli.config.general import (
9
11
  get_general_config,
10
12
  )
11
13
  from gltest_cli.config.types import PluginConfig
14
+ from gltest_cli.config.pytest_context import _pytest_context
12
15
 
13
16
 
14
17
  def pytest_addoption(parser):
@@ -20,17 +23,24 @@ def pytest_addoption(parser):
20
23
  help="Path to directory containing contract files",
21
24
  )
22
25
 
26
+ group.addoption(
27
+ "--artifacts-dir",
28
+ action="store",
29
+ default=None,
30
+ help="Path to directory for storing contract artifacts",
31
+ )
32
+
23
33
  group.addoption(
24
34
  "--default-wait-interval",
25
35
  action="store",
26
- default=10000,
36
+ default=3000,
27
37
  help="Default interval (ms) between transaction receipt checks",
28
38
  )
29
39
 
30
40
  group.addoption(
31
41
  "--default-wait-retries",
32
42
  action="store",
33
- default=15,
43
+ default=50,
34
44
  help="Default number of retries for transaction receipt checks",
35
45
  )
36
46
 
@@ -55,61 +65,123 @@ def pytest_addoption(parser):
55
65
  help="Test with mocks",
56
66
  )
57
67
 
68
+ group.addoption(
69
+ "--leader-only",
70
+ action="store_true",
71
+ default=False,
72
+ help="Run contracts in leader-only mode",
73
+ )
58
74
 
59
- def pytest_configure(config):
60
- general_config = get_general_config()
61
75
 
62
- # Handle user config from gltest.config.yaml
63
- if not user_config_exists():
64
- logger.warning(
65
- "File `gltest.config.yaml` not found in the current directory, using default config"
76
+ def pytest_configure(config):
77
+ try:
78
+ general_config = get_general_config()
79
+
80
+ network_name = config.getoption("--network")
81
+
82
+ if not user_config_exists():
83
+ logger.warning(
84
+ "File `gltest.config.yaml` not found in the current directory, using default config, create a `gltest.config.yaml` file to manage multiple networks"
85
+ )
86
+ user_config = get_default_user_config()
87
+
88
+ # Special handling for testnet_asimov - check if accounts are configured
89
+ if network_name == "testnet_asimov":
90
+ logger.error(
91
+ "For testnet_asimov, you need to configure accounts in gltest.config.yaml, see https://docs.genlayer.com/api-references/genlayer-test"
92
+ )
93
+ pytest.exit("gltest configuration error")
94
+ else:
95
+ logger.info(
96
+ "File `gltest.config.yaml` found in the current directory, using it"
97
+ )
98
+ user_config = load_user_config("gltest.config.yaml")
99
+
100
+ general_config.user_config = user_config
101
+
102
+ # Handle plugin config from command line
103
+ contracts_dir = config.getoption("--contracts-dir")
104
+ artifacts_dir = config.getoption("--artifacts-dir")
105
+ default_wait_interval = config.getoption("--default-wait-interval")
106
+ default_wait_retries = config.getoption("--default-wait-retries")
107
+ rpc_url = config.getoption("--rpc-url")
108
+ network = config.getoption("--network")
109
+ test_with_mocks = config.getoption("--test-with-mocks")
110
+ leader_only = config.getoption("--leader-only")
111
+
112
+ plugin_config = PluginConfig()
113
+ plugin_config.contracts_dir = (
114
+ Path(contracts_dir) if contracts_dir is not None else None
66
115
  )
67
- logger.info("Create a `gltest.config.yaml` file to manage multiple networks")
68
- user_config = get_default_user_config()
69
- else:
70
- logger.info(
71
- "File `gltest.config.yaml` found in the current directory, using it"
116
+ plugin_config.artifacts_dir = (
117
+ Path(artifacts_dir) if artifacts_dir is not None else None
72
118
  )
73
- user_config = load_user_config("gltest.config.yaml")
74
-
75
- general_config.user_config = user_config
76
-
77
- # Handle plugin config from command line
78
- contracts_dir = config.getoption("--contracts-dir")
79
- default_wait_interval = config.getoption("--default-wait-interval")
80
- default_wait_retries = config.getoption("--default-wait-retries")
81
- rpc_url = config.getoption("--rpc-url")
82
- network = config.getoption("--network")
83
- test_with_mocks = config.getoption("--test-with-mocks")
119
+ plugin_config.default_wait_interval = int(default_wait_interval)
120
+ plugin_config.default_wait_retries = int(default_wait_retries)
121
+ plugin_config.rpc_url = rpc_url
122
+ plugin_config.network_name = network
123
+ plugin_config.test_with_mocks = test_with_mocks
124
+ plugin_config.leader_only = leader_only
84
125
 
85
- plugin_config = PluginConfig()
86
- plugin_config.contracts_dir = (
87
- Path(contracts_dir) if contracts_dir is not None else None
88
- )
89
- plugin_config.default_wait_interval = int(default_wait_interval)
90
- plugin_config.default_wait_retries = int(default_wait_retries)
91
- plugin_config.rpc_url = rpc_url
92
- plugin_config.network_name = network
93
- plugin_config.test_with_mocks = test_with_mocks
94
-
95
- general_config.plugin_config = plugin_config
126
+ general_config.plugin_config = plugin_config
127
+ except Exception as e:
128
+ logger.error(f"Gltest configure error: {e}")
129
+ pytest.exit("gltest configuration error")
96
130
 
97
131
 
98
132
  def pytest_sessionstart(session):
99
- general_config = get_general_config()
100
- logger.info("Using the following configuration:")
101
- logger.info(f" RPC URL: {general_config.get_rpc_url()}")
102
- logger.info(f" Selected Network: {general_config.get_network_name()}")
103
- logger.info(
104
- f" Available networks: {list(general_config.user_config.networks.keys())}"
105
- )
106
- logger.info(f" Contracts directory: {general_config.get_contracts_dir()}")
107
- logger.info(f" Environment: {general_config.user_config.environment}")
108
- logger.info(
109
- f" Default wait interval: {general_config.get_default_wait_interval()} ms"
110
- )
111
- logger.info(f" Default wait retries: {general_config.get_default_wait_retries()}")
112
- logger.info(f" Test with mocks: {general_config.get_test_with_mocks()}")
133
+ try:
134
+ general_config = get_general_config()
135
+ artifacts_dir = general_config.get_artifacts_dir()
136
+ if artifacts_dir and artifacts_dir.exists():
137
+ logger.info(f"Clearing artifacts directory: {artifacts_dir}")
138
+ try:
139
+ shutil.rmtree(artifacts_dir)
140
+ artifacts_dir.mkdir(parents=True, exist_ok=True)
141
+ except Exception as e:
142
+ logger.warning(f"Failed to clear artifacts directory: {e}")
143
+ elif artifacts_dir:
144
+ artifacts_dir.mkdir(parents=True, exist_ok=True)
145
+ logger.info("Using the following configuration:")
146
+ logger.info(f" RPC URL: {general_config.get_rpc_url()}")
147
+ logger.info(f" Selected Network: {general_config.get_network_name()}")
148
+ # Show available networks including preconfigured ones
149
+ all_networks = general_config.get_networks_keys()
150
+ logger.info(f" Available networks: {all_networks}")
151
+ logger.info(f" Contracts directory: {general_config.get_contracts_dir()}")
152
+ logger.info(f" Artifacts directory: {general_config.get_artifacts_dir()}")
153
+ logger.info(f" Environment: {general_config.user_config.environment}")
154
+ logger.info(
155
+ f" Default wait interval: {general_config.get_default_wait_interval()} ms"
156
+ )
157
+ logger.info(
158
+ f" Default wait retries: {general_config.get_default_wait_retries()}"
159
+ )
160
+ logger.info(f" Test with mocks: {general_config.get_test_with_mocks()}")
161
+
162
+ if (
163
+ general_config.get_leader_only()
164
+ and not general_config.check_studio_based_rpc()
165
+ ):
166
+ logger.warning(
167
+ "Leader only mode: True (enabled on non-studio network - will have no effect)"
168
+ )
169
+ else:
170
+ logger.info(f" Leader only mode: {general_config.get_leader_only()}")
171
+ except Exception as e:
172
+ logger.error(f"Gltest session start error: {e}")
173
+ pytest.exit("gltest session start error")
174
+
175
+
176
+ def pytest_runtest_setup(item):
177
+ _pytest_context.current_item = item
178
+
179
+
180
+ def pytest_runtest_teardown(item):
181
+ try:
182
+ del _pytest_context.current_item
183
+ except AttributeError:
184
+ pass
113
185
 
114
186
 
115
187
  pytest_plugins = ["gltest.fixtures"]
@@ -0,0 +1,9 @@
1
+ from typing import Optional
2
+ import threading
3
+
4
+ _pytest_context = threading.local()
5
+
6
+
7
+ def get_current_test_nodeid() -> Optional[str]:
8
+ item = getattr(_pytest_context, "current_item", None)
9
+ return item.nodeid if item is not None else None
@@ -2,24 +2,22 @@ from enum import Enum
2
2
  from dataclasses import dataclass, field
3
3
  from pathlib import Path
4
4
  from typing import Dict, List, Optional
5
- from genlayer_py.chains import localnet, testnet_asimov
5
+ from genlayer_py.chains import localnet, studionet, testnet_asimov
6
6
  from genlayer_py.types import GenLayerChain
7
7
  from urllib.parse import urlparse
8
-
9
-
10
- class NetworkConfig(str, Enum):
11
- LOCALNET = "localnet"
12
- TESTNET_ASIMOV = "testnet_asimov"
8
+ from gltest_cli.config.constants import PRECONFIGURED_NETWORKS
13
9
 
14
10
 
15
11
  @dataclass
16
12
  class PluginConfig:
17
13
  contracts_dir: Optional[Path] = None
14
+ artifacts_dir: Optional[Path] = None
18
15
  rpc_url: Optional[str] = None
19
16
  default_wait_interval: Optional[int] = None
20
17
  default_wait_retries: Optional[int] = None
21
18
  network_name: Optional[str] = None
22
19
  test_with_mocks: bool = False
20
+ leader_only: bool = False
23
21
 
24
22
 
25
23
  @dataclass
@@ -28,6 +26,7 @@ class NetworkConfigData:
28
26
  url: Optional[str] = None
29
27
  accounts: Optional[List[str]] = None
30
28
  from_account: Optional[str] = None
29
+ leader_only: bool = False
31
30
 
32
31
  def __post_init__(self):
33
32
  if self.id is not None and not isinstance(self.id, int):
@@ -46,10 +45,13 @@ class NetworkConfigData:
46
45
  @dataclass
47
46
  class PathConfig:
48
47
  contracts: Optional[Path] = None
48
+ artifacts: Optional[Path] = None
49
49
 
50
50
  def __post_init__(self):
51
51
  if self.contracts is not None and not isinstance(self.contracts, (str, Path)):
52
52
  raise ValueError("contracts must be a string or Path")
53
+ if self.artifacts is not None and not isinstance(self.artifacts, (str, Path)):
54
+ raise ValueError("artifacts must be a string or Path")
53
55
 
54
56
 
55
57
  @dataclass
@@ -93,10 +95,29 @@ class GeneralConfig:
93
95
  def set_contracts_dir(self, contracts_dir: Path):
94
96
  self.plugin_config.contracts_dir = contracts_dir
95
97
 
98
+ def get_artifacts_dir(self) -> Path:
99
+ if self.plugin_config.artifacts_dir is not None:
100
+ return self.plugin_config.artifacts_dir
101
+ return self.user_config.paths.artifacts
102
+
103
+ def set_artifacts_dir(self, artifacts_dir: Path):
104
+ self.plugin_config.artifacts_dir = artifacts_dir
105
+
106
+ def get_analysis_dir(self) -> Path:
107
+ artifacts_dir = self.get_artifacts_dir()
108
+ return artifacts_dir / "analysis"
109
+
110
+ def get_networks_keys(self) -> List[str]:
111
+ return list(self.user_config.networks.keys())
112
+
96
113
  def get_rpc_url(self) -> str:
97
114
  if self.plugin_config.rpc_url is not None:
98
115
  return self.plugin_config.rpc_url
99
116
  network_name = self.get_network_name()
117
+ if network_name not in self.user_config.networks:
118
+ raise ValueError(
119
+ f"Unknown network: {network_name}, possible values: {self.get_networks_keys()}"
120
+ )
100
121
  return self.user_config.networks[network_name].url
101
122
 
102
123
  def get_default_account_key(self, network_name: Optional[str] = None) -> str:
@@ -110,16 +131,37 @@ class GeneralConfig:
110
131
  return self.user_config.networks[self.user_config.default_network].accounts
111
132
 
112
133
  def get_chain(self) -> GenLayerChain:
134
+ network_name = self.get_network_name()
135
+ if network_name not in self.user_config.networks:
136
+ raise ValueError(
137
+ f"Unknown network: {network_name}, possible values: {self.get_networks_keys()}"
138
+ )
139
+
140
+ # Reserved network names
141
+ chain_map_by_name = {
142
+ "localnet": localnet,
143
+ "studionet": studionet,
144
+ "testnet_asimov": testnet_asimov,
145
+ }
146
+
147
+ if network_name in chain_map_by_name:
148
+ return chain_map_by_name[network_name]
149
+
150
+ if network_name in PRECONFIGURED_NETWORKS:
151
+ raise ValueError(
152
+ f"Network {network_name} should be handled by reserved mapping"
153
+ )
154
+
155
+ # Custom networks
113
156
  chain_map_by_id = {
114
157
  61999: localnet,
115
158
  4221: testnet_asimov,
116
159
  }
117
- network_name = self.get_network_name()
118
160
  network_id = self.user_config.networks[network_name].id
119
161
  if network_id not in chain_map_by_id:
120
162
  known = ", ".join(map(str, chain_map_by_id.keys()))
121
163
  raise ValueError(
122
- f"Unknown network: {network_name}, possible values: {known}"
164
+ f"Unknown network id: {network_id}, possible values: {known}"
123
165
  )
124
166
  return chain_map_by_id[network_id]
125
167
 
@@ -141,8 +183,31 @@ class GeneralConfig:
141
183
  def get_test_with_mocks(self) -> bool:
142
184
  return self.plugin_config.test_with_mocks
143
185
 
186
+ def get_leader_only(self) -> bool:
187
+ if self.plugin_config.leader_only:
188
+ return True
189
+ network_name = self.get_network_name()
190
+ if network_name in self.user_config.networks:
191
+ network_config = self.user_config.networks[network_name]
192
+ return network_config.leader_only
193
+ return False
194
+
144
195
  def check_local_rpc(self) -> bool:
145
196
  SUPPORTED_RPC_DOMAINS = ["localhost", "127.0.0.1"]
146
197
  rpc_url = self.get_rpc_url()
147
198
  domain = urlparse(rpc_url).netloc.split(":")[0] # Extract domain without port
148
199
  return domain in SUPPORTED_RPC_DOMAINS
200
+
201
+ def check_studio_based_rpc(self) -> bool:
202
+ SUPPORTED_RPC_DOMAINS = ["localhost", "127.0.0.1"]
203
+ rpc_url = self.get_rpc_url()
204
+ domain = urlparse(rpc_url).netloc.split(":")[0] # Extract domain without port
205
+
206
+ if domain in SUPPORTED_RPC_DOMAINS:
207
+ return True
208
+
209
+ # Check .genlayer.com or .genlayerlabs.com subdomains
210
+ if domain.endswith(".genlayer.com") or domain.endswith(".genlayerlabs.com"):
211
+ return True
212
+
213
+ return False