genlayer-test 0.11.0__py3-none-any.whl → 0.13.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.
@@ -0,0 +1,258 @@
1
+ """
2
+ Mock implementation of _genlayer_wasi module.
3
+
4
+ Provides drop-in replacements for WASI functions that contracts use:
5
+ - storage_read / storage_write
6
+ - get_balance / get_self_balance
7
+ - gl_call
8
+
9
+ This module is injected into sys.modules before importing contracts.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import io
15
+ import os
16
+ import threading
17
+ import warnings
18
+ from typing import TYPE_CHECKING, Any
19
+
20
+ if TYPE_CHECKING:
21
+ from .vm import VMContext
22
+
23
+ # Thread-local VM context for parallel test safety
24
+ _local = threading.local()
25
+
26
+ # Original os.fdopen reference (saved once at module load)
27
+ _original_fdopen = os.fdopen
28
+
29
+
30
+ def set_vm(vm: "VMContext") -> None:
31
+ """Set the active VM context for WASI operations."""
32
+ _local.vm = vm
33
+ _local.fd_counter = 100
34
+ _local.fd_buffers = {}
35
+
36
+
37
+ def get_vm() -> "VMContext":
38
+ """Get the active VM context."""
39
+ vm = getattr(_local, 'vm', None)
40
+ if vm is None:
41
+ raise RuntimeError("No VM context active. Call VMContext.activate() first.")
42
+ return vm
43
+
44
+
45
+ def clear_vm() -> None:
46
+ """Clear the VM context and clean up resources."""
47
+ if hasattr(_local, 'fd_buffers'):
48
+ for buf in _local.fd_buffers.values():
49
+ buf.close()
50
+ _local.fd_buffers.clear()
51
+ _local.vm = None
52
+ _local.fd_counter = 100
53
+
54
+
55
+ def storage_read(slot: bytes, off: int, buf: bytearray, /) -> None:
56
+ """Read from storage slot into buffer."""
57
+ vm = get_vm()
58
+ data = vm._storage.do_read(slot, off, len(buf))
59
+ buf[:] = data
60
+
61
+
62
+ def storage_write(slot: bytes, off: int, what: bytes, /) -> None:
63
+ """Write to storage slot."""
64
+ vm = get_vm()
65
+ vm._storage.do_write(slot, off, what)
66
+
67
+
68
+ def get_balance(address: bytes, /) -> int:
69
+ """Get balance of an address."""
70
+ vm = get_vm()
71
+ return vm._balances.get(address, 0)
72
+
73
+
74
+ def get_self_balance() -> int:
75
+ """Get balance of current contract."""
76
+ vm = get_vm()
77
+ contract_addr = vm._contract_address
78
+ if contract_addr is None:
79
+ return 0
80
+ addr_bytes = bytes(contract_addr) if hasattr(contract_addr, '__bytes__') else contract_addr
81
+ return vm._balances.get(addr_bytes, 0)
82
+
83
+
84
+ def gl_call(data: bytes, /) -> int:
85
+ """
86
+ Execute a GenVM call operation.
87
+
88
+ Returns file descriptor for reading response, or 2^32-1 on failure.
89
+ """
90
+ vm = get_vm()
91
+ fd_buffers = getattr(_local, 'fd_buffers', {})
92
+
93
+ try:
94
+ from genlayer.py import calldata
95
+ request = calldata.decode(data)
96
+ except Exception as e:
97
+ vm._trace(f"gl_call decode error: {e}")
98
+ return 2**32 - 1
99
+
100
+ response = _handle_gl_call(vm, request)
101
+
102
+ if response is None:
103
+ return 2**32 - 1
104
+
105
+ # If response is already bytes (from RunNondet), use directly
106
+ # RunNondet handles its own ResultCode prefix for sub-VM result format
107
+ if isinstance(response, bytes):
108
+ encoded = response
109
+ else:
110
+ # Regular responses (web, llm, etc) are just calldata-encoded
111
+ # The SDK's _decode_nondet expects plain {"ok": ...} format
112
+ try:
113
+ from genlayer.py import calldata
114
+ encoded = calldata.encode(response)
115
+ except Exception as e:
116
+ vm._trace(f"gl_call encode error: {e}")
117
+ return 2**32 - 1
118
+
119
+ fd_counter = getattr(_local, 'fd_counter', 100)
120
+ fd = fd_counter
121
+ _local.fd_counter = fd_counter + 1
122
+
123
+ buf = io.BytesIO(encoded)
124
+ fd_buffers[fd] = buf
125
+ _local.fd_buffers = fd_buffers
126
+
127
+ return fd
128
+
129
+
130
+ def _handle_gl_call(vm: "VMContext", request: Any) -> Any:
131
+ """Handle a gl_call request and return the response."""
132
+ if not isinstance(request, dict):
133
+ return None
134
+
135
+ if "Return" in request:
136
+ vm._return_value = request["Return"]
137
+ vm._returned = True
138
+ return None
139
+
140
+ if "Rollback" in request:
141
+ raise ContractRollback(request["Rollback"])
142
+
143
+ if "Trace" in request:
144
+ trace_data = request["Trace"]
145
+ if "Message" in trace_data:
146
+ vm._trace(trace_data["Message"])
147
+ return {"ok": None}
148
+
149
+ if "Sandbox" in request:
150
+ warnings.warn(
151
+ "gl.sandbox is not fully isolated in direct test mode.",
152
+ RuntimeWarning,
153
+ stacklevel=3,
154
+ )
155
+ return {"ok": None}
156
+
157
+ if "RunNondet" in request:
158
+ return _handle_run_nondet(vm, request["RunNondet"])
159
+
160
+ if "GetWebsite" in request or "WebRequest" in request:
161
+ web_data = request.get("GetWebsite") or request.get("WebRequest", {})
162
+ return _handle_web_request(vm, web_data)
163
+
164
+ if "ExecPrompt" in request:
165
+ prompt_data = request["ExecPrompt"]
166
+ return _handle_llm_request(vm, prompt_data)
167
+
168
+ vm._trace(f"Unknown gl_call request type: {list(request.keys())}")
169
+ return None
170
+
171
+
172
+ def _handle_web_request(vm: "VMContext", data: Any) -> Any:
173
+ """Handle web request using mocks."""
174
+ url = data.get("url", "")
175
+ method = data.get("method", "GET")
176
+
177
+ mock_data = vm._match_web_mock(url, method)
178
+ if mock_data:
179
+ # SDK expects {"ok": {"response": {...}}} format
180
+ # Mock stores {"response": {...}, "method": "..."}
181
+ if "response" in mock_data:
182
+ return {"ok": {"response": mock_data["response"]}}
183
+ return {"ok": mock_data}
184
+
185
+ raise MockNotFoundError(f"No web mock for {method} {url}")
186
+
187
+
188
+ def _handle_llm_request(vm: "VMContext", data: Any) -> Any:
189
+ """Handle LLM prompt request using mocks."""
190
+ prompt = data.get("prompt", "")
191
+
192
+ response = vm._match_llm_mock(prompt)
193
+ if response:
194
+ return {"ok": response}
195
+
196
+ raise MockNotFoundError(f"No LLM mock for prompt: {prompt[:100]}...")
197
+
198
+
199
+ def _handle_run_nondet(vm: "VMContext", data: Any) -> Any:
200
+ """Handle RunNondet request by executing the leader function directly.
201
+
202
+ In direct mode, we skip the leader/validator consensus and just run
203
+ the leader function, returning its result.
204
+ """
205
+ import cloudpickle
206
+ from genlayer.py import calldata
207
+
208
+ data_leader = data.get("data_leader")
209
+ if not data_leader:
210
+ raise ValueError("RunNondet missing data_leader")
211
+
212
+ # Unpickle and execute the leader function
213
+ # The lambda expects a stage_data argument, but in leader mode it's None
214
+ leader_fn = cloudpickle.loads(data_leader)
215
+
216
+ try:
217
+ result = leader_fn(None)
218
+ # Wrap result in Return format (code 0 + calldata)
219
+ encoded = bytes([0]) + calldata.encode(result)
220
+ return encoded
221
+ except Exception as e:
222
+ # Wrap error in UserError format (code 1 + message)
223
+ error_msg = str(e)
224
+ return bytes([1]) + error_msg.encode('utf-8')
225
+
226
+
227
+ def patched_fdopen(fd_arg: int, mode: str = "r", *args, **kwargs):
228
+ """Patched os.fdopen that intercepts mock file descriptors."""
229
+ fd_buffers = getattr(_local, 'fd_buffers', {})
230
+
231
+ if fd_arg in fd_buffers:
232
+ buf = fd_buffers.pop(fd_arg)
233
+ buf.seek(0)
234
+ return buf
235
+
236
+ return _original_fdopen(fd_arg, mode, *args, **kwargs)
237
+
238
+
239
+ class ContractRollback(Exception):
240
+ """Raised when a contract calls gl.rollback()."""
241
+
242
+ def __init__(self, message: str):
243
+ self.message = message
244
+ super().__init__(message)
245
+
246
+
247
+ class MockNotFoundError(Exception):
248
+ """Raised when no mock is found for a nondet operation."""
249
+ pass
250
+
251
+
252
+ __all__ = (
253
+ "storage_read",
254
+ "storage_write",
255
+ "get_balance",
256
+ "get_self_balance",
257
+ "gl_call",
258
+ )