pythsol 0.1.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 (76) hide show
  1. pythsol/__init__.py +317 -0
  2. pythsol/cli/__init__.py +7 -0
  3. pythsol/cli/cache.py +78 -0
  4. pythsol/cli/cluster.py +45 -0
  5. pythsol/cli/commands/build.py +214 -0
  6. pythsol/cli/commands/check.py +54 -0
  7. pythsol/cli/commands/client_cmd.py +97 -0
  8. pythsol/cli/commands/deploy.py +103 -0
  9. pythsol/cli/commands/docs_cmd.py +62 -0
  10. pythsol/cli/commands/doctor.py +211 -0
  11. pythsol/cli/commands/emit_c.py +69 -0
  12. pythsol/cli/commands/faucet.py +66 -0
  13. pythsol/cli/commands/idl_cmd.py +69 -0
  14. pythsol/cli/commands/idl_publish.py +80 -0
  15. pythsol/cli/commands/init.py +65 -0
  16. pythsol/cli/commands/port_cmd.py +42 -0
  17. pythsol/cli/commands/profile.py +57 -0
  18. pythsol/cli/commands/serve.py +52 -0
  19. pythsol/cli/commands/test_cmd.py +217 -0
  20. pythsol/cli/commands/verify.py +96 -0
  21. pythsol/cli/commands/wallet.py +128 -0
  22. pythsol/cli/config.py +84 -0
  23. pythsol/cli/main.py +72 -0
  24. pythsol/cli/rpc.py +56 -0
  25. pythsol/cli/template_programs/__init__.py +1 -0
  26. pythsol/cli/template_programs/counter.py +52 -0
  27. pythsol/cli/template_programs/escrow.py +75 -0
  28. pythsol/cli/template_programs/ledger.py +68 -0
  29. pythsol/cli/template_programs/multisig.py +91 -0
  30. pythsol/cli/template_programs/poll.py +70 -0
  31. pythsol/cli/template_programs/vault.py +67 -0
  32. pythsol/cli/templates.py +192 -0
  33. pythsol/client/__init__.py +5 -0
  34. pythsol/client/from_idl.py +356 -0
  35. pythsol/client/gen.py +496 -0
  36. pythsol/client/port.py +355 -0
  37. pythsol/compiler/__init__.py +39 -0
  38. pythsol/compiler/diagnostics.py +130 -0
  39. pythsol/compiler/emit.py +3550 -0
  40. pythsol/compiler/front.py +84 -0
  41. pythsol/compiler/idl.py +465 -0
  42. pythsol/compiler/model.py +1329 -0
  43. pythsol/compiler/modules.py +320 -0
  44. pythsol/compiler/parse.py +59 -0
  45. pythsol/compiler/pipeline.py +152 -0
  46. pythsol/compiler/resolve.py +518 -0
  47. pythsol/compiler/subset.py +1061 -0
  48. pythsol/compiler/toolchain.py +438 -0
  49. pythsol/compiler/typecheck.py +2447 -0
  50. pythsol/playground/__init__.py +7 -0
  51. pythsol/playground/app.css +182 -0
  52. pythsol/playground/app.js +260 -0
  53. pythsol/playground/apple-touch-icon.png +0 -0
  54. pythsol/playground/docs.html +61 -0
  55. pythsol/playground/favicon-32.png +0 -0
  56. pythsol/playground/favicon.ico +0 -0
  57. pythsol/playground/favicon.svg +24 -0
  58. pythsol/playground/hero-bg.webp +0 -0
  59. pythsol/playground/index.html +66 -0
  60. pythsol/playground/landing.html +810 -0
  61. pythsol/playground/playground.html +77 -0
  62. pythsol/playground/robots.txt +4 -0
  63. pythsol/playground/server.py +336 -0
  64. pythsol/playground/site-bg.svg +146 -0
  65. pythsol/playground/site.css +1105 -0
  66. pythsol/py.typed +0 -0
  67. pythsol/refrt/__init__.py +200 -0
  68. pythsol/runtime/__init__.py +17 -0
  69. pythsol/runtime/pythsol_rt.h +1216 -0
  70. pythsol/testing/__init__.py +12 -0
  71. pythsol/testing/plugin.py +437 -0
  72. pythsol-0.1.0.dist-info/METADATA +178 -0
  73. pythsol-0.1.0.dist-info/RECORD +76 -0
  74. pythsol-0.1.0.dist-info/WHEEL +4 -0
  75. pythsol-0.1.0.dist-info/entry_points.txt +6 -0
  76. pythsol-0.1.0.dist-info/licenses/LICENSE +202 -0
pythsol/client/gen.py ADDED
@@ -0,0 +1,496 @@
1
+ """Render a solders client from a :class:`ProgramModel`.
2
+
3
+ The text this module returns imports only the standard library and
4
+ ``solders``. It does not import the compiler, so a generated
5
+ ``*_client.py`` can ship next to a program without a PythSol install
6
+ beyond what the caller already uses to build transactions.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from pythsol.compiler.model import (
12
+ ACCOUNT_DATA_OFFSET,
13
+ AccountModel,
14
+ DataParamModel,
15
+ EventModel,
16
+ FieldModel,
17
+ InstructionModel,
18
+ ProgramModel,
19
+ StructModel,
20
+ snake_case,
21
+ )
22
+
23
+ __all__ = ["render_client"]
24
+
25
+ _SYSTEM_PROGRAM = "11111111111111111111111111111111"
26
+ _TOKEN_PROGRAM = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
27
+ _TOKEN_2022_PROGRAM = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"
28
+ _ATA_PROGRAM = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
29
+
30
+ #: ``struct.pack`` format for scalar widths that fit in one call.
31
+ _PACK_FMT: dict[str, str] = {
32
+ "uint8_t": "<B",
33
+ "uint16_t": "<H",
34
+ "uint32_t": "<I",
35
+ "uint64_t": "<Q",
36
+ "int8_t": "<b",
37
+ "int16_t": "<h",
38
+ "int32_t": "<i",
39
+ "int64_t": "<q",
40
+ "bool": "<B",
41
+ }
42
+
43
+ _UNPACK_FMT: dict[str, str] = dict(_PACK_FMT)
44
+
45
+
46
+ def render_client(model: ProgramModel) -> str:
47
+ """Return the full ``{name}_client.py`` source for ``model``."""
48
+ lines: list[str] = [
49
+ '"""Generated by PythSol. Do not edit.',
50
+ "",
51
+ f"Program `{model.name}` from `{model.source}`.",
52
+ "Build transactions with solders. Decode account bytes with `decode_*`.",
53
+ '"""',
54
+ "",
55
+ "from __future__ import annotations",
56
+ "",
57
+ "import struct",
58
+ "",
59
+ "from solders.instruction import AccountMeta, Instruction",
60
+ "from solders.pubkey import Pubkey",
61
+ "",
62
+ f"PROGRAM_NAME = {model.snake_name!r}",
63
+ f'_SYSTEM_PROGRAM = Pubkey.from_string("{_SYSTEM_PROGRAM}")',
64
+ "",
65
+ ]
66
+ if any(instr.needs_token or instr.needs_ata for instr in model.instructions):
67
+ lines.append(f'_TOKEN_PROGRAM = Pubkey.from_string("{_TOKEN_PROGRAM}")')
68
+ if any(instr.needs_token2022 for instr in model.instructions):
69
+ lines.append(f'_TOKEN_2022_PROGRAM = Pubkey.from_string("{_TOKEN_2022_PROGRAM}")')
70
+ if any(instr.needs_ata for instr in model.instructions):
71
+ lines.append(f'_ATA_PROGRAM = Pubkey.from_string("{_ATA_PROGRAM}")')
72
+ if any(
73
+ instr.needs_token or instr.needs_token2022 or instr.needs_ata
74
+ for instr in model.instructions
75
+ ):
76
+ lines.append("")
77
+ lines.extend(_enum_constants(model))
78
+ if model.errors:
79
+ lines.append("# Custom program errors (6000 + declaration index).")
80
+ for err in model.errors:
81
+ const = f"ERR_{err.owner.upper()}_{err.name.upper()}"
82
+ lines.append(f"{const} = {err.code} # {err.owner}.{err.name}")
83
+ lines.append("")
84
+ for instr in model.instructions:
85
+ const = f"_DISC_{instr.snake_name.upper()}"
86
+ lines.append(f"{const} = {_bytes_expr(instr.discriminator)}")
87
+ lines.append("")
88
+ if _has_string_arg(model):
89
+ lines.extend(_fixed_string_helper())
90
+ lines.append("")
91
+ for instr in model.instructions:
92
+ lines.extend(_instruction_fn(instr))
93
+ lines.append("")
94
+ structs = {item.name: item for item in model.structs}
95
+ payloads = {item.name: item.payload_c for item in model.enums if item.payload_c}
96
+ for struct in model.structs:
97
+ lines.extend(_struct_decode_fn(struct, structs, payloads))
98
+ lines.append("")
99
+ for account in model.accounts:
100
+ lines.extend(_decode_fn(account, structs, payloads))
101
+ lines.append("")
102
+ if model.events:
103
+ lines.extend(_event_decoder(model.events))
104
+ lines.append("")
105
+ text = "\n".join(lines).rstrip() + "\n"
106
+ return text
107
+
108
+
109
+ def _enum_constants(model: ProgramModel) -> list[str]:
110
+ """``STATUS_OPEN = 0`` for each unit-enum variant (the one-byte tag)."""
111
+ if not model.enums:
112
+ return []
113
+ used: set[str] = set()
114
+ lines = ["# Unit enum tags (declaration index, one byte)."]
115
+ for enum in model.enums:
116
+ for variant, tag in enum.variants:
117
+ base = f"{enum.name.upper()}_{variant.upper()}"
118
+ const = base if base not in used else f"{base}_{tag}"
119
+ used.add(const)
120
+ lines.append(f"{const} = {tag} # {enum.name}.{variant}")
121
+ lines.append("")
122
+ return lines
123
+
124
+
125
+ def _bytes_expr(data: bytes) -> str:
126
+ inner = ", ".join(str(byte) for byte in data)
127
+ return f"bytes(({inner}))"
128
+
129
+
130
+ def _py_type(c_type: str) -> str:
131
+ if c_type == "SolPubkey":
132
+ return "Pubkey"
133
+ if c_type == "bool":
134
+ return "bool"
135
+ return "int"
136
+
137
+
138
+ def _has_string_arg(model: ProgramModel) -> bool:
139
+ return any(
140
+ param.shape == "string" for instr in model.instructions for param in instr.data_params
141
+ )
142
+
143
+
144
+ def _fixed_string_helper() -> list[str]:
145
+ """One packer for every ``FixedString[N]`` instruction argument."""
146
+ return [
147
+ "def _pack_fixed_str(text: str, capacity: int) -> bytes:",
148
+ ' """u32 byte length plus ``capacity`` reserved payload bytes."""',
149
+ ' raw = text.encode("utf-8")',
150
+ " if len(raw) > capacity:",
151
+ " raise ValueError(",
152
+ ' f"string is {len(raw)} bytes; FixedString[{capacity}] holds {capacity}"',
153
+ " )",
154
+ ' return struct.pack("<I", len(raw)) + raw + bytes(capacity - len(raw))',
155
+ ]
156
+
157
+
158
+ def _param_type(param: DataParamModel) -> str:
159
+ inner = _py_type(param.c_type)
160
+ if param.shape == "option":
161
+ return f"{inner} | None"
162
+ if param.shape == "string":
163
+ return "str"
164
+ return inner
165
+
166
+
167
+ def _pack_expr(param: DataParamModel, name: str) -> str:
168
+ if param.shape == "option":
169
+ present = _pack_scalar(param.c_type, name)
170
+ width = param.elem_size
171
+ return (
172
+ f"(bytes([0]) + bytes({width})) if {name} is None else (bytes([1]) + {present})"
173
+ )
174
+ if param.shape == "string":
175
+ return f"_pack_fixed_str({name}, {param.length})"
176
+ return _pack_scalar(param.c_type, name)
177
+
178
+
179
+ def _pack_scalar(c_type: str, name: str) -> str:
180
+ if c_type == "SolPubkey":
181
+ return f"bytes({name})"
182
+ if c_type == "uint128_t":
183
+ return f'{name}.to_bytes(16, "little")'
184
+ if c_type == "int128_t":
185
+ return f'{name}.to_bytes(16, "little", signed=True)'
186
+ if c_type == "bool":
187
+ return f'struct.pack("<B", 1 if {name} else 0)'
188
+ fmt = _PACK_FMT[c_type]
189
+ return f'struct.pack("{fmt}", {name})'
190
+
191
+
192
+ def _instruction_fn(instr: InstructionModel) -> list[str]:
193
+ used = {"program_id"}
194
+ params: list[tuple[str, str]] = [("program_id", "Pubkey")]
195
+ account_names: list[tuple[str, InstructionArg]] = []
196
+ for account in instr.account_params:
197
+ py_name = _unique(account.name, used)
198
+ params.append((py_name, "Pubkey"))
199
+ account_names.append((py_name, InstructionArg(account.kind, account.writable)))
200
+ data_names: list[tuple[str, DataParamModel]] = []
201
+ for data in instr.data_params:
202
+ py_name = _unique(data.name, used)
203
+ params.append((py_name, _param_type(data)))
204
+ data_names.append((py_name, data))
205
+
206
+ lines = _signature(instr.snake_name, params, "Instruction")
207
+ lines.append(f' """Build the `{instr.snake_name}` instruction."""')
208
+ pays = any(account.is_init for account in instr.account_params)
209
+ needs_system = pays or instr.needs_system
210
+ if data_names:
211
+ parts = [f"_DISC_{instr.snake_name.upper()}"]
212
+ parts.extend(_pack_expr(param, name) for name, param in data_names)
213
+ lines.append(" data = b''.join((")
214
+ for part in parts:
215
+ lines.append(f" {part},")
216
+ lines.append(" ))")
217
+ else:
218
+ lines.append(f" data = _DISC_{instr.snake_name.upper()}")
219
+ lines.append(" accounts = [")
220
+ for name, arg in account_names:
221
+ signer = arg.kind == "signer"
222
+ writable = arg.writable or (signer and pays)
223
+ lines.append(
224
+ f" AccountMeta({name}, is_signer={signer}, is_writable={writable}),"
225
+ )
226
+ if needs_system:
227
+ lines.append(
228
+ " AccountMeta(_SYSTEM_PROGRAM, is_signer=False, is_writable=False),"
229
+ )
230
+ if instr.needs_token or instr.needs_ata:
231
+ lines.append(
232
+ " AccountMeta(_TOKEN_PROGRAM, is_signer=False, is_writable=False),"
233
+ )
234
+ if instr.needs_token2022:
235
+ lines.append(
236
+ " AccountMeta(_TOKEN_2022_PROGRAM, is_signer=False, is_writable=False),"
237
+ )
238
+ if instr.needs_ata:
239
+ lines.append(
240
+ " AccountMeta(_ATA_PROGRAM, is_signer=False, is_writable=False),"
241
+ )
242
+ lines.append(" ]")
243
+ lines.append(" return Instruction(program_id=program_id, data=data, accounts=accounts)")
244
+ return lines
245
+
246
+
247
+ class InstructionArg:
248
+ """Signer/writable facts the client needs for one account parameter."""
249
+
250
+ def __init__(self, kind: str, writable: bool) -> None:
251
+ self.kind = kind
252
+ self.writable = writable
253
+
254
+
255
+ def _decode_fn(
256
+ account: AccountModel,
257
+ structs: dict[str, StructModel],
258
+ payloads: dict[str, str] | None = None,
259
+ ) -> list[str]:
260
+ fn = f"decode_{snake_case(account.name)}"
261
+ doc = f"Decode a `{account.name}` account (discriminator plus fields)."
262
+ if any(field.shape == "option" for field in account.fields):
263
+ doc += " Option fields are (tag, value); the value bytes are always reserved."
264
+ if any(field.shape == "string" for field in account.fields):
265
+ doc += " Fixed strings are UTF-8 text; the reserved bytes stay on the account."
266
+ if any(field.shape == "struct" for field in account.fields):
267
+ doc += " Struct fields are dicts packed inline, with no extra discriminator."
268
+ lines = [
269
+ f"def {fn}(data: bytes) -> dict[str, object]:",
270
+ f' """{doc}"""',
271
+ f" if len(data) < {account.size}:",
272
+ f' raise ValueError("{account.name} account is shorter than {account.size} bytes")',
273
+ ]
274
+ for field in account.fields:
275
+ lines.extend(
276
+ _decode_field(field, ACCOUNT_DATA_OFFSET + field.offset, structs, payloads)
277
+ )
278
+ lines.append(" return {")
279
+ for field in account.fields:
280
+ lines.append(f' "{field.name}": {field.name},')
281
+ lines.append(" }")
282
+ return lines
283
+
284
+
285
+ def _struct_decode_fn(
286
+ struct: StructModel,
287
+ structs: dict[str, StructModel],
288
+ payloads: dict[str, str] | None = None,
289
+ ) -> list[str]:
290
+ """Decode one ``@struct`` at a caller-supplied absolute offset."""
291
+ lines = [
292
+ f"def _decode_struct_{struct.name}(data: bytes, base: int) -> dict[str, object]:",
293
+ f' """Decode `{struct.name}` at `base` (fixed width, no discriminator)."""',
294
+ ]
295
+ for field in struct.fields:
296
+ lines.extend(_decode_field(field, f"base + {field.offset}", structs, payloads))
297
+ lines.append(" return {")
298
+ for field in struct.fields:
299
+ lines.append(f' "{field.name}": {field.name},')
300
+ lines.append(" }")
301
+ return lines
302
+
303
+
304
+ def _decode_field(
305
+ field: FieldModel,
306
+ base: int | str,
307
+ structs: dict[str, StructModel],
308
+ payloads: dict[str, str] | None = None,
309
+ ) -> list[str]:
310
+ payload_c = "" if payloads is None else payloads.get(field.defined, "")
311
+ if field.shape == "scalar" and payload_c and field.size > 1:
312
+ at = base + 1 if isinstance(base, int) else f"({base}) + 1"
313
+ fmt = _UNPACK_FMT[payload_c]
314
+ return [
315
+ f" {field.name}_tag = data[{base}]",
316
+ f' ({field.name}_value,) = struct.unpack_from("{fmt}", data, {at})',
317
+ f" {field.name} = ({field.name}_tag, {field.name}_value)",
318
+ ]
319
+ if field.shape == "struct":
320
+ if field.defined not in structs:
321
+ return [
322
+ f' raise ValueError("unknown struct {field.defined}")',
323
+ ]
324
+ return [f" {field.name} = _decode_struct_{field.defined}(data, {base})"]
325
+ if field.shape == "array":
326
+ return _decode_array(field, base)
327
+ if field.shape == "vec":
328
+ return _decode_vec(field, base)
329
+ if field.shape == "option":
330
+ return _decode_option(field, base)
331
+ if field.shape == "string":
332
+ return _decode_string(field, base)
333
+ return [_read_scalar(field.name, field.c_type, base, field.elem_size or field.size)]
334
+
335
+
336
+ def _plus(base: int | str, delta: int) -> int | str:
337
+ """Absolute offset ``base + delta``. An int stays an int so account lines do not change."""
338
+ if isinstance(base, int):
339
+ return base + delta
340
+ if delta == 0:
341
+ return base
342
+ return f"({base}) + {delta}"
343
+
344
+
345
+ def _decode_string(field: FieldModel, base: int | str) -> list[str]:
346
+ """``u32`` length at ``base``, reserved bytes at ``base + 4``. Length past ``N`` raises."""
347
+ length = f"_str_{field.name}_len"
348
+ raw = f"_str_{field.name}_raw"
349
+ payload = _plus(base, 4)
350
+ return [
351
+ f' ({length},) = struct.unpack_from("<I", data, {base})',
352
+ f" if {length} > {field.length}:",
353
+ f' raise ValueError("{field.name} is longer than {field.length} bytes")',
354
+ f" {raw} = data[{payload}:{payload} + {length}]",
355
+ f' {field.name} = {raw}.decode("utf-8")',
356
+ ]
357
+
358
+
359
+ def _decode_option(field: FieldModel, base: int | str) -> list[str]:
360
+ """Tag byte at ``base``, reserved inner value at ``base + 1``."""
361
+ tag = f"_opt_{field.name}_tag"
362
+ value = f"_opt_{field.name}_value"
363
+ lines = [f" {tag} = data[{base}]"]
364
+ lines.append(_read_scalar(value, field.c_type, _plus(base, 1), field.elem_size))
365
+ lines.append(f" {field.name} = ({tag}, {value})")
366
+ return lines
367
+
368
+
369
+ def _decode_vec(field: FieldModel, base: int | str) -> list[str]:
370
+ """``u32`` length at ``base``, reserved elements at ``base + 4``."""
371
+ step = field.elem_size or 1
372
+ payload = _plus(base, 4)
373
+ length = f"_vec_{field.name}_len"
374
+ py = _py_type(field.c_type)
375
+ lines = [
376
+ f' ({length},) = struct.unpack_from("<I", data, {base})',
377
+ f" if {length} > {field.length}:",
378
+ f' raise ValueError("{field.name} is longer than {field.length} elements")',
379
+ f" {field.name}: list[{py}] = []",
380
+ f" for _index in range({length}):",
381
+ f" _at = {payload} + _index * {step}",
382
+ ]
383
+ elem = field.c_type
384
+ if elem == "SolPubkey":
385
+ lines.append(f" {field.name}.append(Pubkey.from_bytes(data[_at:_at + 32]))")
386
+ return lines
387
+ if elem in ("uint128_t", "int128_t"):
388
+ signed = "True" if elem == "int128_t" else "False"
389
+ lines.append(
390
+ f" {field.name}.append("
391
+ f'int.from_bytes(data[_at:_at + 16], "little", signed={signed}))'
392
+ )
393
+ return lines
394
+ if elem == "bool":
395
+ lines.append(f" {field.name}.append(bool(data[_at]))")
396
+ return lines
397
+ fmt = _UNPACK_FMT[elem]
398
+ lines.append(f' (_item,) = struct.unpack_from("{fmt}", data, _at)')
399
+ lines.append(f" {field.name}.append(_item)")
400
+ return lines
401
+
402
+
403
+ def _decode_array(field: FieldModel, base: int | str) -> list[str]:
404
+ elem = field.c_type
405
+ py = _py_type(elem)
406
+ step = field.elem_size or field.size // field.length
407
+ at = f"{base} + _index * {step}" if isinstance(base, int) else f"({base}) + _index * {step}"
408
+ lines = [f" {field.name}: list[{py}] = []"]
409
+ lines.append(f" for _index in range({field.length}):")
410
+ lines.append(f" _at = {at}")
411
+ if elem == "SolPubkey":
412
+ lines.append(f" {field.name}.append(Pubkey.from_bytes(data[_at:_at + 32]))")
413
+ return lines
414
+ if elem in ("uint128_t", "int128_t"):
415
+ signed = "True" if elem == "int128_t" else "False"
416
+ lines.append(
417
+ f" {field.name}.append("
418
+ f'int.from_bytes(data[_at:_at + 16], "little", signed={signed}))'
419
+ )
420
+ return lines
421
+ if elem == "bool":
422
+ lines.append(f" {field.name}.append(bool(data[_at]))")
423
+ return lines
424
+ fmt = _UNPACK_FMT[elem]
425
+ lines.append(f' (_item,) = struct.unpack_from("{fmt}", data, _at)')
426
+ lines.append(f" {field.name}.append(_item)")
427
+ return lines
428
+
429
+
430
+ def _read_scalar(name: str, c_type: str, offset: int | str, size: int) -> str:
431
+ if c_type == "SolPubkey":
432
+ end = _plus(offset, 32)
433
+ return f" {name} = Pubkey.from_bytes(data[{offset}:{end}])"
434
+ if c_type == "uint128_t":
435
+ end = _plus(offset, 16)
436
+ return f' {name} = int.from_bytes(data[{offset}:{end}], "little")'
437
+ if c_type == "int128_t":
438
+ end = _plus(offset, 16)
439
+ return f' {name} = int.from_bytes(data[{offset}:{end}], "little", signed=True)'
440
+ if c_type == "bool":
441
+ return f" {name} = bool(data[{offset}])"
442
+ if size == 1 and c_type == "uint8_t":
443
+ return f" {name} = data[{offset}]"
444
+ fmt = _UNPACK_FMT[c_type]
445
+ return f' ({name},) = struct.unpack_from("{fmt}", data, {offset})'
446
+
447
+
448
+ def _event_decoder(events: list[EventModel]) -> list[str]:
449
+ """``decode_event`` reads one ``sol_log_data`` slice (disc plus payload)."""
450
+ lines = ["# Event discriminators: sha256('event:' + name)[:8]."]
451
+ for event in events:
452
+ lines.append(f"_EVENT_{event.name.upper()} = {_bytes_expr(event.discriminator)}")
453
+ lines.append("")
454
+ lines.extend(
455
+ [
456
+ "def decode_event(data: bytes) -> dict[str, object]:",
457
+ ' """Decode one event log (8-byte discriminator, then packed fields)."""',
458
+ " if len(data) < 8:",
459
+ ' raise ValueError("event is shorter than 8 bytes")',
460
+ " disc = data[:8]",
461
+ ]
462
+ )
463
+ for event in events:
464
+ lines.append(f" if disc == _EVENT_{event.name.upper()}:")
465
+ for field in event.fields:
466
+ raw = _read_scalar(field.name, field.c_type, 8 + field.offset, field.size)
467
+ lines.append(" " + raw)
468
+ lines.append(" return {")
469
+ lines.append(f' "name": {event.name!r},')
470
+ lines.append(' "fields": {')
471
+ for field in event.fields:
472
+ lines.append(f' "{field.name}": {field.name},')
473
+ lines.append(" },")
474
+ lines.append(" }")
475
+ lines.append(' raise ValueError("unknown event")')
476
+ return lines
477
+
478
+
479
+ def _signature(name: str, params: list[tuple[str, str]], result: str) -> list[str]:
480
+ parts = [f"{param}: {ann}" for param, ann in params]
481
+ one = f"def {name}(" + ", ".join(parts) + f") -> {result}:"
482
+ if len(one) <= 100:
483
+ return [one]
484
+ lines = [f"def {name}("]
485
+ for part in parts:
486
+ lines.append(f" {part},")
487
+ lines.append(f") -> {result}:")
488
+ return lines
489
+
490
+
491
+ def _unique(name: str, used: set[str]) -> str:
492
+ candidate = name
493
+ while candidate in used:
494
+ candidate += "_"
495
+ used.add(candidate)
496
+ return candidate