genlayer-test 0.4.0__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.
- {genlayer_test-0.4.0.dist-info → genlayer_test-0.5.0.dist-info}/METADATA +257 -24
- genlayer_test-0.5.0.dist-info/RECORD +76 -0
- gltest/__init__.py +7 -6
- gltest/{glchain/client.py → clients.py} +1 -1
- gltest/contracts/__init__.py +4 -0
- gltest/contracts/contract.py +205 -0
- gltest/{glchain/contract.py → contracts/contract_factory.py} +47 -144
- gltest/contracts/contract_functions.py +62 -0
- gltest/contracts/method_stats.py +163 -0
- gltest/contracts/stats_collector.py +259 -0
- gltest/contracts/utils.py +12 -0
- gltest/fixtures.py +2 -6
- gltest/helpers/take_snapshot.py +1 -1
- gltest/logging.py +17 -0
- gltest/types.py +1 -0
- gltest_cli/config/constants.py +2 -0
- gltest_cli/config/plugin.py +121 -49
- gltest_cli/config/pytest_context.py +9 -0
- gltest_cli/config/types.py +73 -8
- gltest_cli/config/user.py +71 -28
- gltest_cli/logging.py +4 -3
- tests/examples/contracts/football_prediction_market.py +1 -1
- tests/examples/tests/test_football_prediction_market.py +2 -2
- tests/examples/tests/test_intelligent_oracle_factory.py +8 -24
- tests/examples/tests/test_llm_erc20.py +5 -5
- tests/examples/tests/test_llm_erc20_analyze.py +50 -0
- tests/examples/tests/test_log_indexer.py +23 -11
- tests/examples/tests/test_multi_file_contract.py +2 -2
- tests/examples/tests/test_multi_file_contract_legacy.py +2 -2
- tests/examples/tests/test_multi_read_erc20.py +14 -12
- tests/examples/tests/test_multi_tenant_storage.py +11 -7
- tests/examples/tests/test_read_erc20.py +1 -1
- tests/examples/tests/test_storage.py +4 -4
- tests/examples/tests/test_storage_legacy.py +5 -3
- tests/examples/tests/test_user_storage.py +20 -10
- tests/examples/tests/test_wizard_of_coin.py +1 -1
- tests/gltest_cli/config/test_config_integration.py +432 -0
- tests/gltest_cli/config/test_general_config.py +406 -0
- tests/gltest_cli/config/test_plugin.py +164 -1
- tests/gltest_cli/config/test_user.py +61 -1
- genlayer_test-0.4.0.dist-info/RECORD +0 -66
- gltest/glchain/__init__.py +0 -16
- {genlayer_test-0.4.0.dist-info → genlayer_test-0.5.0.dist-info}/WHEEL +0 -0
- {genlayer_test-0.4.0.dist-info → genlayer_test-0.5.0.dist-info}/entry_points.txt +0 -0
- {genlayer_test-0.4.0.dist-info → genlayer_test-0.5.0.dist-info}/licenses/LICENSE +0 -0
- {genlayer_test-0.4.0.dist-info → genlayer_test-0.5.0.dist-info}/top_level.txt +0 -0
- /gltest/{glchain/account.py → accounts.py} +0 -0
@@ -0,0 +1,205 @@
|
|
1
|
+
import types
|
2
|
+
from eth_account.signers.local import LocalAccount
|
3
|
+
from dataclasses import dataclass
|
4
|
+
from gltest.clients import get_gl_client
|
5
|
+
from gltest.types import (
|
6
|
+
CalldataEncodable,
|
7
|
+
GenLayerTransaction,
|
8
|
+
TransactionStatus,
|
9
|
+
TransactionHashVariant,
|
10
|
+
)
|
11
|
+
from typing import List, Any, Optional, Dict, Callable
|
12
|
+
from gltest_cli.config.general import get_general_config
|
13
|
+
from .contract_functions import ContractFunction
|
14
|
+
from .stats_collector import StatsCollector, SimulationConfig
|
15
|
+
|
16
|
+
|
17
|
+
def read_contract_wrapper(
|
18
|
+
self,
|
19
|
+
method_name: str,
|
20
|
+
args: Optional[List[CalldataEncodable]] = None,
|
21
|
+
) -> Any:
|
22
|
+
"""
|
23
|
+
Wrapper to the contract read method.
|
24
|
+
"""
|
25
|
+
|
26
|
+
def call_method(
|
27
|
+
transaction_hash_variant: TransactionHashVariant = TransactionHashVariant.LATEST_NONFINAL,
|
28
|
+
):
|
29
|
+
client = get_gl_client()
|
30
|
+
return client.read_contract(
|
31
|
+
address=self.address,
|
32
|
+
function_name=method_name,
|
33
|
+
account=self.account,
|
34
|
+
args=args,
|
35
|
+
transaction_hash_variant=transaction_hash_variant,
|
36
|
+
)
|
37
|
+
|
38
|
+
return ContractFunction(
|
39
|
+
method_name=method_name,
|
40
|
+
read_only=True,
|
41
|
+
call_method=call_method,
|
42
|
+
)
|
43
|
+
|
44
|
+
|
45
|
+
def write_contract_wrapper(
|
46
|
+
self,
|
47
|
+
method_name: str,
|
48
|
+
args: Optional[List[CalldataEncodable]] = None,
|
49
|
+
) -> GenLayerTransaction:
|
50
|
+
"""
|
51
|
+
Wrapper to the contract write method.
|
52
|
+
"""
|
53
|
+
|
54
|
+
def transact_method(
|
55
|
+
value: int = 0,
|
56
|
+
consensus_max_rotations: Optional[int] = None,
|
57
|
+
wait_transaction_status: TransactionStatus = TransactionStatus.ACCEPTED,
|
58
|
+
wait_interval: Optional[int] = None,
|
59
|
+
wait_retries: Optional[int] = None,
|
60
|
+
wait_triggered_transactions: bool = False,
|
61
|
+
wait_triggered_transactions_status: TransactionStatus = TransactionStatus.ACCEPTED,
|
62
|
+
):
|
63
|
+
"""
|
64
|
+
Transact the contract method.
|
65
|
+
"""
|
66
|
+
general_config = get_general_config()
|
67
|
+
actual_wait_interval = (
|
68
|
+
wait_interval
|
69
|
+
if wait_interval is not None
|
70
|
+
else general_config.get_default_wait_interval()
|
71
|
+
)
|
72
|
+
actual_wait_retries = (
|
73
|
+
wait_retries
|
74
|
+
if wait_retries is not None
|
75
|
+
else general_config.get_default_wait_retries()
|
76
|
+
)
|
77
|
+
leader_only = (
|
78
|
+
general_config.get_leader_only()
|
79
|
+
if general_config.check_studio_based_rpc()
|
80
|
+
else False
|
81
|
+
)
|
82
|
+
client = get_gl_client()
|
83
|
+
tx_hash = client.write_contract(
|
84
|
+
address=self.address,
|
85
|
+
function_name=method_name,
|
86
|
+
account=self.account,
|
87
|
+
value=value,
|
88
|
+
consensus_max_rotations=consensus_max_rotations,
|
89
|
+
leader_only=leader_only,
|
90
|
+
args=args,
|
91
|
+
)
|
92
|
+
receipt = client.wait_for_transaction_receipt(
|
93
|
+
transaction_hash=tx_hash,
|
94
|
+
status=wait_transaction_status,
|
95
|
+
interval=actual_wait_interval,
|
96
|
+
retries=actual_wait_retries,
|
97
|
+
)
|
98
|
+
if wait_triggered_transactions:
|
99
|
+
triggered_transactions = receipt["triggered_transactions"]
|
100
|
+
for triggered_transaction in triggered_transactions:
|
101
|
+
client.wait_for_transaction_receipt(
|
102
|
+
transaction_hash=triggered_transaction,
|
103
|
+
status=wait_triggered_transactions_status,
|
104
|
+
interval=actual_wait_interval,
|
105
|
+
retries=actual_wait_retries,
|
106
|
+
)
|
107
|
+
return receipt
|
108
|
+
|
109
|
+
def analyze_method(
|
110
|
+
provider: str,
|
111
|
+
model: str,
|
112
|
+
config: Optional[Dict[str, Any]] = None,
|
113
|
+
plugin: Optional[str] = None,
|
114
|
+
plugin_config: Optional[Dict[str, Any]] = None,
|
115
|
+
runs: int = 100,
|
116
|
+
):
|
117
|
+
"""
|
118
|
+
Analyze the contract method using StatsCollector.
|
119
|
+
"""
|
120
|
+
collector = StatsCollector(
|
121
|
+
contract_address=self.address,
|
122
|
+
method_name=method_name,
|
123
|
+
account=self.account,
|
124
|
+
args=args,
|
125
|
+
)
|
126
|
+
sim_config = SimulationConfig(
|
127
|
+
provider=provider,
|
128
|
+
model=model,
|
129
|
+
config=config,
|
130
|
+
plugin=plugin,
|
131
|
+
plugin_config=plugin_config,
|
132
|
+
)
|
133
|
+
sim_results = collector.run_simulations(sim_config, runs)
|
134
|
+
return collector.analyze_results(sim_results, runs, sim_config)
|
135
|
+
|
136
|
+
return ContractFunction(
|
137
|
+
method_name=method_name,
|
138
|
+
read_only=False,
|
139
|
+
transact_method=transact_method,
|
140
|
+
analyze_method=analyze_method,
|
141
|
+
)
|
142
|
+
|
143
|
+
|
144
|
+
def contract_function_factory(method_name: str, read_only: bool) -> Callable:
|
145
|
+
"""
|
146
|
+
Create a function that interacts with a specific contract method.
|
147
|
+
"""
|
148
|
+
if read_only:
|
149
|
+
return lambda self, args=None: read_contract_wrapper(self, method_name, args)
|
150
|
+
return lambda self, args=None: write_contract_wrapper(self, method_name, args)
|
151
|
+
|
152
|
+
|
153
|
+
@dataclass
|
154
|
+
class Contract:
|
155
|
+
"""
|
156
|
+
Class to interact with a contract, its methods
|
157
|
+
are implemented dynamically at build time.
|
158
|
+
"""
|
159
|
+
|
160
|
+
address: str
|
161
|
+
account: Optional[LocalAccount] = None
|
162
|
+
_schema: Optional[Dict[str, Any]] = None
|
163
|
+
|
164
|
+
@classmethod
|
165
|
+
def new(
|
166
|
+
cls,
|
167
|
+
address: str,
|
168
|
+
schema: Dict[str, Any],
|
169
|
+
account: Optional[LocalAccount] = None,
|
170
|
+
) -> "Contract":
|
171
|
+
"""
|
172
|
+
Build the methods from the schema.
|
173
|
+
"""
|
174
|
+
if not isinstance(schema, dict) or "methods" not in schema:
|
175
|
+
raise ValueError("Invalid schema: must contain 'methods' field")
|
176
|
+
instance = cls(address=address, _schema=schema, account=account)
|
177
|
+
instance._build_methods_from_schema()
|
178
|
+
return instance
|
179
|
+
|
180
|
+
def _build_methods_from_schema(self):
|
181
|
+
"""
|
182
|
+
Build the methods from the schema.
|
183
|
+
"""
|
184
|
+
if self._schema is None:
|
185
|
+
raise ValueError("No schema provided")
|
186
|
+
for method_name, method_info in self._schema["methods"].items():
|
187
|
+
if not isinstance(method_info, dict) or "readonly" not in method_info:
|
188
|
+
raise ValueError(
|
189
|
+
f"Invalid method info for '{method_name}': must contain 'readonly' field"
|
190
|
+
)
|
191
|
+
method_func = contract_function_factory(
|
192
|
+
method_name, method_info["readonly"]
|
193
|
+
)
|
194
|
+
bound_method = types.MethodType(method_func, self)
|
195
|
+
setattr(self, method_name, bound_method)
|
196
|
+
|
197
|
+
def connect(self, account: LocalAccount) -> "Contract":
|
198
|
+
"""
|
199
|
+
Create a new instance of the contract with the same methods and a different account.
|
200
|
+
"""
|
201
|
+
new_contract = self.__class__(
|
202
|
+
address=self.address, account=account, _schema=self._schema
|
203
|
+
)
|
204
|
+
new_contract._build_methods_from_schema()
|
205
|
+
return new_contract
|
@@ -1,145 +1,26 @@
|
|
1
|
+
from dataclasses import dataclass
|
2
|
+
from typing import Type, Union, Optional, List, Any
|
3
|
+
from pathlib import Path
|
1
4
|
from eth_typing import (
|
2
5
|
Address,
|
3
6
|
ChecksumAddress,
|
4
7
|
)
|
5
8
|
from eth_account.signers.local import LocalAccount
|
6
|
-
from typing import Union
|
7
|
-
from pathlib import Path
|
8
|
-
from dataclasses import dataclass
|
9
9
|
from gltest.artifacts import (
|
10
10
|
find_contract_definition_from_name,
|
11
11
|
find_contract_definition_from_path,
|
12
12
|
)
|
13
|
+
from gltest.clients import (
|
14
|
+
get_gl_client,
|
15
|
+
get_gl_hosted_studio_client,
|
16
|
+
get_local_client,
|
17
|
+
)
|
18
|
+
from .contract import Contract
|
19
|
+
from gltest.logging import logger
|
20
|
+
from gltest.types import TransactionStatus
|
13
21
|
from gltest.assertions import tx_execution_failed
|
14
22
|
from gltest.exceptions import DeploymentError
|
15
|
-
from .client import get_gl_client, get_gl_hosted_studio_client, get_local_client
|
16
|
-
from gltest.types import CalldataEncodable, GenLayerTransaction, TransactionStatus
|
17
|
-
from typing import List, Any, Type, Optional, Dict, Callable
|
18
|
-
import types
|
19
23
|
from gltest_cli.config.general import get_general_config
|
20
|
-
from gltest_cli.logging import logger
|
21
|
-
|
22
|
-
|
23
|
-
@dataclass
|
24
|
-
class Contract:
|
25
|
-
"""
|
26
|
-
Class to interact with a contract, its methods
|
27
|
-
are implemented dynamically at build time.
|
28
|
-
"""
|
29
|
-
|
30
|
-
address: str
|
31
|
-
account: Optional[LocalAccount] = None
|
32
|
-
_schema: Optional[Dict[str, Any]] = None
|
33
|
-
|
34
|
-
@classmethod
|
35
|
-
def new(
|
36
|
-
cls,
|
37
|
-
address: str,
|
38
|
-
schema: Dict[str, Any],
|
39
|
-
account: Optional[LocalAccount] = None,
|
40
|
-
) -> "Contract":
|
41
|
-
"""
|
42
|
-
Build the methods from the schema.
|
43
|
-
"""
|
44
|
-
if not isinstance(schema, dict) or "methods" not in schema:
|
45
|
-
raise ValueError("Invalid schema: must contain 'methods' field")
|
46
|
-
instance = cls(address=address, _schema=schema, account=account)
|
47
|
-
instance._build_methods_from_schema()
|
48
|
-
return instance
|
49
|
-
|
50
|
-
def _build_methods_from_schema(self):
|
51
|
-
if self._schema is None:
|
52
|
-
raise ValueError("No schema provided")
|
53
|
-
for method_name, method_info in self._schema["methods"].items():
|
54
|
-
if not isinstance(method_info, dict) or "readonly" not in method_info:
|
55
|
-
raise ValueError(
|
56
|
-
f"Invalid method info for '{method_name}': must contain 'readonly' field"
|
57
|
-
)
|
58
|
-
method_func = self.contract_method_factory(
|
59
|
-
method_name, method_info["readonly"]
|
60
|
-
)
|
61
|
-
bound_method = types.MethodType(method_func, self)
|
62
|
-
setattr(self, method_name, bound_method)
|
63
|
-
|
64
|
-
def connect(self, account: LocalAccount) -> "Contract":
|
65
|
-
"""
|
66
|
-
Create a new instance of the contract with the same methods and a different account.
|
67
|
-
"""
|
68
|
-
new_contract = self.__class__(
|
69
|
-
address=self.address, account=account, _schema=self._schema
|
70
|
-
)
|
71
|
-
new_contract._build_methods_from_schema()
|
72
|
-
return new_contract
|
73
|
-
|
74
|
-
@staticmethod
|
75
|
-
def contract_method_factory(method_name: str, read_only: bool) -> Callable:
|
76
|
-
"""
|
77
|
-
Create a function that interacts with a specific contract method.
|
78
|
-
"""
|
79
|
-
|
80
|
-
def read_contract_wrapper(
|
81
|
-
self,
|
82
|
-
args: Optional[List[CalldataEncodable]] = None,
|
83
|
-
) -> Any:
|
84
|
-
"""
|
85
|
-
Wrapper to the contract read method.
|
86
|
-
"""
|
87
|
-
client = get_gl_client()
|
88
|
-
return client.read_contract(
|
89
|
-
address=self.address,
|
90
|
-
function_name=method_name,
|
91
|
-
account=self.account,
|
92
|
-
args=args,
|
93
|
-
)
|
94
|
-
|
95
|
-
def write_contract_wrapper(
|
96
|
-
self,
|
97
|
-
args: Optional[List[CalldataEncodable]] = None,
|
98
|
-
value: int = 0,
|
99
|
-
consensus_max_rotations: Optional[int] = None,
|
100
|
-
leader_only: bool = False,
|
101
|
-
wait_transaction_status: TransactionStatus = TransactionStatus.FINALIZED,
|
102
|
-
wait_interval: Optional[int] = None,
|
103
|
-
wait_retries: Optional[int] = None,
|
104
|
-
wait_triggered_transactions: bool = True,
|
105
|
-
wait_triggered_transactions_status: TransactionStatus = TransactionStatus.FINALIZED,
|
106
|
-
) -> GenLayerTransaction:
|
107
|
-
"""
|
108
|
-
Wrapper to the contract write method.
|
109
|
-
"""
|
110
|
-
general_config = get_general_config()
|
111
|
-
if wait_interval is None:
|
112
|
-
wait_interval = general_config.get_default_wait_interval()
|
113
|
-
if wait_retries is None:
|
114
|
-
wait_retries = general_config.get_default_wait_retries()
|
115
|
-
client = get_gl_client()
|
116
|
-
tx_hash = client.write_contract(
|
117
|
-
address=self.address,
|
118
|
-
function_name=method_name,
|
119
|
-
account=self.account,
|
120
|
-
value=value,
|
121
|
-
consensus_max_rotations=consensus_max_rotations,
|
122
|
-
leader_only=leader_only,
|
123
|
-
args=args,
|
124
|
-
)
|
125
|
-
receipt = client.wait_for_transaction_receipt(
|
126
|
-
transaction_hash=tx_hash,
|
127
|
-
status=wait_transaction_status,
|
128
|
-
interval=wait_interval,
|
129
|
-
retries=wait_retries,
|
130
|
-
)
|
131
|
-
if wait_triggered_transactions:
|
132
|
-
triggered_transactions = receipt["triggered_transactions"]
|
133
|
-
for triggered_transaction in triggered_transactions:
|
134
|
-
client.wait_for_transaction_receipt(
|
135
|
-
transaction_hash=triggered_transaction,
|
136
|
-
status=wait_triggered_transactions_status,
|
137
|
-
interval=wait_interval,
|
138
|
-
retries=wait_retries,
|
139
|
-
)
|
140
|
-
return receipt
|
141
|
-
|
142
|
-
return read_contract_wrapper if read_only else write_contract_wrapper
|
143
24
|
|
144
25
|
|
145
26
|
@dataclass
|
@@ -184,17 +65,17 @@ class ContractFactory:
|
|
184
65
|
"""Attempts to get the contract schema using multiple clients in a fallback pattern.
|
185
66
|
|
186
67
|
This method tries to get the contract schema in the following order:
|
187
|
-
1.
|
188
|
-
2.
|
189
|
-
3.
|
68
|
+
1. Default client
|
69
|
+
2. Hosted studio client
|
70
|
+
3. Local client
|
190
71
|
|
191
72
|
Returns:
|
192
73
|
Optional[Dict[str, Any]]: The contract schema if successful, None if all attempts fail.
|
193
74
|
"""
|
194
75
|
clients = (
|
76
|
+
("default", get_gl_client()),
|
195
77
|
("hosted studio", get_gl_hosted_studio_client()),
|
196
78
|
("local", get_local_client()),
|
197
|
-
("default", get_gl_client()),
|
198
79
|
)
|
199
80
|
for label, client in clients:
|
200
81
|
try:
|
@@ -216,7 +97,7 @@ class ContractFactory:
|
|
216
97
|
schema = self._get_schema_with_fallback()
|
217
98
|
if schema is None:
|
218
99
|
raise ValueError(
|
219
|
-
"Failed to get schema from all clients (hosted studio,
|
100
|
+
"Failed to get schema from all clients (default, hosted studio, and local)"
|
220
101
|
)
|
221
102
|
|
222
103
|
return Contract.new(address=contract_address, schema=schema, account=account)
|
@@ -226,19 +107,31 @@ class ContractFactory:
|
|
226
107
|
args: List[Any] = [],
|
227
108
|
account: Optional[LocalAccount] = None,
|
228
109
|
consensus_max_rotations: Optional[int] = None,
|
229
|
-
leader_only: bool = False,
|
230
110
|
wait_interval: Optional[int] = None,
|
231
111
|
wait_retries: Optional[int] = None,
|
232
|
-
wait_transaction_status: TransactionStatus = TransactionStatus.
|
112
|
+
wait_transaction_status: TransactionStatus = TransactionStatus.ACCEPTED,
|
113
|
+
wait_triggered_transactions: bool = False,
|
114
|
+
wait_triggered_transactions_status: TransactionStatus = TransactionStatus.ACCEPTED,
|
233
115
|
) -> Contract:
|
234
116
|
"""
|
235
117
|
Deploy the contract
|
236
118
|
"""
|
237
119
|
general_config = get_general_config()
|
238
|
-
|
239
|
-
wait_interval
|
240
|
-
|
241
|
-
|
120
|
+
actual_wait_interval = (
|
121
|
+
wait_interval
|
122
|
+
if wait_interval is not None
|
123
|
+
else general_config.get_default_wait_interval()
|
124
|
+
)
|
125
|
+
actual_wait_retries = (
|
126
|
+
wait_retries
|
127
|
+
if wait_retries is not None
|
128
|
+
else general_config.get_default_wait_retries()
|
129
|
+
)
|
130
|
+
leader_only = (
|
131
|
+
general_config.get_leader_only()
|
132
|
+
if general_config.check_studio_based_rpc()
|
133
|
+
else False
|
134
|
+
)
|
242
135
|
|
243
136
|
client = get_gl_client()
|
244
137
|
try:
|
@@ -252,14 +145,24 @@ class ContractFactory:
|
|
252
145
|
tx_receipt = client.wait_for_transaction_receipt(
|
253
146
|
transaction_hash=tx_hash,
|
254
147
|
status=wait_transaction_status,
|
255
|
-
interval=
|
256
|
-
retries=
|
148
|
+
interval=actual_wait_interval,
|
149
|
+
retries=actual_wait_retries,
|
257
150
|
)
|
258
151
|
if tx_execution_failed(tx_receipt):
|
259
152
|
raise ValueError(
|
260
153
|
f"Deployment transaction finalized with error: {tx_receipt}"
|
261
154
|
)
|
262
155
|
|
156
|
+
if wait_triggered_transactions:
|
157
|
+
triggered_transactions = tx_receipt["triggered_transactions"]
|
158
|
+
for triggered_transaction in triggered_transactions:
|
159
|
+
client.wait_for_transaction_receipt(
|
160
|
+
transaction_hash=triggered_transaction,
|
161
|
+
status=wait_triggered_transactions_status,
|
162
|
+
interval=actual_wait_interval,
|
163
|
+
retries=actual_wait_retries,
|
164
|
+
)
|
165
|
+
|
263
166
|
if (
|
264
167
|
"tx_data_decoded" in tx_receipt
|
265
168
|
and "contract_address" in tx_receipt["tx_data_decoded"]
|
@@ -273,7 +176,7 @@ class ContractFactory:
|
|
273
176
|
schema = self._get_schema_with_fallback()
|
274
177
|
if schema is None:
|
275
178
|
raise ValueError(
|
276
|
-
"Failed to get schema from all clients (hosted studio,
|
179
|
+
"Failed to get schema from all clients (default, hosted studio, and local)"
|
277
180
|
)
|
278
181
|
|
279
182
|
return Contract.new(
|
@@ -0,0 +1,62 @@
|
|
1
|
+
from dataclasses import dataclass
|
2
|
+
from typing import Callable, Optional, Dict, Any
|
3
|
+
from gltest.types import TransactionStatus, TransactionHashVariant
|
4
|
+
|
5
|
+
|
6
|
+
@dataclass
|
7
|
+
class ContractFunction:
|
8
|
+
method_name: str
|
9
|
+
read_only: bool
|
10
|
+
call_method: Optional[Callable] = None
|
11
|
+
analyze_method: Optional[Callable] = None
|
12
|
+
transact_method: Optional[Callable] = None
|
13
|
+
|
14
|
+
def call(
|
15
|
+
self,
|
16
|
+
transaction_hash_variant: TransactionHashVariant = TransactionHashVariant.LATEST_NONFINAL,
|
17
|
+
):
|
18
|
+
if not self.read_only:
|
19
|
+
raise ValueError("call() not implemented for non-readonly method")
|
20
|
+
return self.call_method(transaction_hash_variant=transaction_hash_variant)
|
21
|
+
|
22
|
+
def transact(
|
23
|
+
self,
|
24
|
+
value: int = 0,
|
25
|
+
consensus_max_rotations: Optional[int] = None,
|
26
|
+
wait_transaction_status: TransactionStatus = TransactionStatus.ACCEPTED,
|
27
|
+
wait_interval: Optional[int] = None,
|
28
|
+
wait_retries: Optional[int] = None,
|
29
|
+
wait_triggered_transactions: bool = False,
|
30
|
+
wait_triggered_transactions_status: TransactionStatus = TransactionStatus.ACCEPTED,
|
31
|
+
):
|
32
|
+
if self.read_only:
|
33
|
+
raise ValueError("Cannot transact read-only method")
|
34
|
+
return self.transact_method(
|
35
|
+
value=value,
|
36
|
+
consensus_max_rotations=consensus_max_rotations,
|
37
|
+
wait_transaction_status=wait_transaction_status,
|
38
|
+
wait_interval=wait_interval,
|
39
|
+
wait_retries=wait_retries,
|
40
|
+
wait_triggered_transactions=wait_triggered_transactions,
|
41
|
+
wait_triggered_transactions_status=wait_triggered_transactions_status,
|
42
|
+
)
|
43
|
+
|
44
|
+
def analyze(
|
45
|
+
self,
|
46
|
+
provider: str,
|
47
|
+
model: str,
|
48
|
+
config: Optional[Dict[str, Any]] = None,
|
49
|
+
plugin: Optional[str] = None,
|
50
|
+
plugin_config: Optional[Dict[str, Any]] = None,
|
51
|
+
runs: int = 100,
|
52
|
+
):
|
53
|
+
if self.read_only:
|
54
|
+
raise ValueError("Cannot analyze read-only method")
|
55
|
+
return self.analyze_method(
|
56
|
+
provider=provider,
|
57
|
+
model=model,
|
58
|
+
config=config,
|
59
|
+
plugin=plugin,
|
60
|
+
plugin_config=plugin_config,
|
61
|
+
runs=runs,
|
62
|
+
)
|
@@ -0,0 +1,163 @@
|
|
1
|
+
from dataclasses import dataclass
|
2
|
+
from typing import List, Any, Optional, Dict
|
3
|
+
import json
|
4
|
+
from pathlib import Path
|
5
|
+
from .utils import safe_filename
|
6
|
+
|
7
|
+
|
8
|
+
@dataclass
|
9
|
+
class MethodStatsSummary:
|
10
|
+
"""Statistical analysis results for a contract method execution."""
|
11
|
+
|
12
|
+
method: str
|
13
|
+
args: List[Any]
|
14
|
+
total_runs: int
|
15
|
+
executed_runs: int
|
16
|
+
server_error_runs: int
|
17
|
+
failed_runs: int
|
18
|
+
successful_runs: int
|
19
|
+
unique_states: int
|
20
|
+
most_common_state_count: int
|
21
|
+
reliability_score: float
|
22
|
+
execution_time: float
|
23
|
+
provider: str
|
24
|
+
model: str
|
25
|
+
|
26
|
+
@property
|
27
|
+
def success_rate(self) -> float:
|
28
|
+
"""Calculate success rate as percentage."""
|
29
|
+
if self.total_runs == 0:
|
30
|
+
return 0.0
|
31
|
+
return (self.successful_runs / self.total_runs) * 100
|
32
|
+
|
33
|
+
def __str__(self) -> str:
|
34
|
+
"""Format the statistical analysis results."""
|
35
|
+
return f"""Method analysis summary
|
36
|
+
---------------------------
|
37
|
+
Method: {self.method}
|
38
|
+
Args: {self.args}
|
39
|
+
Provider: {self.provider}
|
40
|
+
Model: {self.model}
|
41
|
+
Total runs: {self.total_runs}
|
42
|
+
Server error runs: {self.server_error_runs}
|
43
|
+
Method executed runs: {self.executed_runs}
|
44
|
+
Method successful runs: {self.successful_runs}
|
45
|
+
Method failed runs: {self.failed_runs}
|
46
|
+
Unique states: {self.unique_states}
|
47
|
+
Reliability score: {self.reliability_score:.2f}% ({self.most_common_state_count}/{self.executed_runs} consistent)
|
48
|
+
Execution time: {self.execution_time:.1f}s"""
|
49
|
+
|
50
|
+
|
51
|
+
@dataclass
|
52
|
+
class StateGroup:
|
53
|
+
"""Represents a group of runs with the same contract state."""
|
54
|
+
|
55
|
+
count: int
|
56
|
+
state_hash: str
|
57
|
+
|
58
|
+
|
59
|
+
@dataclass
|
60
|
+
class FailedRun:
|
61
|
+
"""Represents a failed run with error details."""
|
62
|
+
|
63
|
+
run: int
|
64
|
+
error: str
|
65
|
+
error_type: str # "server" or "simulation"
|
66
|
+
genvm_result: Optional[Dict[str, str]]
|
67
|
+
|
68
|
+
|
69
|
+
@dataclass
|
70
|
+
class MethodStatsDetailed:
|
71
|
+
"""Detailed statistical analysis results for a contract method execution."""
|
72
|
+
|
73
|
+
method: str
|
74
|
+
params: List[Any]
|
75
|
+
timestamp: str
|
76
|
+
configuration: Dict[str, Any]
|
77
|
+
execution_time: float
|
78
|
+
executed_runs: int
|
79
|
+
failed_runs: int
|
80
|
+
successful_runs: int
|
81
|
+
server_error_runs: int
|
82
|
+
most_common_state_count: int
|
83
|
+
reliability_score: float
|
84
|
+
sim_results: List[Any]
|
85
|
+
unique_states: int
|
86
|
+
state_groups: List[StateGroup]
|
87
|
+
failed_runs_results: List[FailedRun]
|
88
|
+
|
89
|
+
def to_dict(self) -> Dict[str, Any]:
|
90
|
+
"""Convert to dictionary format."""
|
91
|
+
return {
|
92
|
+
"method": self.method,
|
93
|
+
"params": self.params,
|
94
|
+
"timestamp": self.timestamp,
|
95
|
+
"configuration": self.configuration,
|
96
|
+
"execution_time": self.execution_time,
|
97
|
+
"method_executed_runs": self.executed_runs,
|
98
|
+
"method_failed_runs": self.failed_runs,
|
99
|
+
"method_successful_runs": self.successful_runs,
|
100
|
+
"server_error_runs": self.server_error_runs,
|
101
|
+
"most_common_state_count": self.most_common_state_count,
|
102
|
+
"reliability_score": self.reliability_score,
|
103
|
+
"unique_states": self.unique_states,
|
104
|
+
"state_groups": [
|
105
|
+
{"count": sg.count, "state_hash": sg.state_hash}
|
106
|
+
for sg in self.state_groups
|
107
|
+
],
|
108
|
+
"failed_runs": [
|
109
|
+
{
|
110
|
+
"run": fr.run,
|
111
|
+
"error": fr.error,
|
112
|
+
"error_type": fr.error_type,
|
113
|
+
"genvm_result": fr.genvm_result,
|
114
|
+
}
|
115
|
+
for fr in self.failed_runs_results
|
116
|
+
],
|
117
|
+
"simulation_results": self.filter_sim_results(),
|
118
|
+
}
|
119
|
+
|
120
|
+
def filter_sim_results(self) -> List[Any]:
|
121
|
+
"""Filter the simulation results to only include specific fields."""
|
122
|
+
filtered_results = []
|
123
|
+
allowed_fields = [
|
124
|
+
"calldata",
|
125
|
+
"contract_state",
|
126
|
+
"eq_outputs",
|
127
|
+
"execution_result",
|
128
|
+
"genvm_result",
|
129
|
+
"node_config",
|
130
|
+
"pending_transactions",
|
131
|
+
"result",
|
132
|
+
]
|
133
|
+
|
134
|
+
for result in self.sim_results:
|
135
|
+
filtered_result = {
|
136
|
+
key: result.get(key) for key in allowed_fields if key in result
|
137
|
+
}
|
138
|
+
filtered_results.append(filtered_result)
|
139
|
+
|
140
|
+
return filtered_results
|
141
|
+
|
142
|
+
def save_to_directory(self, directory: str, filename: Optional[str] = None) -> str:
|
143
|
+
"""
|
144
|
+
Save the detailed stats to a JSON file in the specified directory.
|
145
|
+
|
146
|
+
Raises:
|
147
|
+
OSError: If directory creation or file writing fails.
|
148
|
+
"""
|
149
|
+
directory_path = Path(directory)
|
150
|
+
directory_path.mkdir(parents=True, exist_ok=True)
|
151
|
+
if filename is None:
|
152
|
+
safe_method = safe_filename(self.method)
|
153
|
+
safe_timestamp = safe_filename(self.timestamp)
|
154
|
+
filename = f"{safe_method}_{safe_timestamp}.json"
|
155
|
+
if not filename.endswith(".json"):
|
156
|
+
filename += ".json"
|
157
|
+
filepath = directory_path / filename
|
158
|
+
try:
|
159
|
+
with open(filepath, "w") as f:
|
160
|
+
json.dump(self.to_dict(), f, indent=2)
|
161
|
+
except (OSError, TypeError) as e:
|
162
|
+
raise OSError(f"Failed to save stats to {filepath}: {e}") from e
|
163
|
+
return str(filepath)
|