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.
- pythsol/__init__.py +317 -0
- pythsol/cli/__init__.py +7 -0
- pythsol/cli/cache.py +78 -0
- pythsol/cli/cluster.py +45 -0
- pythsol/cli/commands/build.py +214 -0
- pythsol/cli/commands/check.py +54 -0
- pythsol/cli/commands/client_cmd.py +97 -0
- pythsol/cli/commands/deploy.py +103 -0
- pythsol/cli/commands/docs_cmd.py +62 -0
- pythsol/cli/commands/doctor.py +211 -0
- pythsol/cli/commands/emit_c.py +69 -0
- pythsol/cli/commands/faucet.py +66 -0
- pythsol/cli/commands/idl_cmd.py +69 -0
- pythsol/cli/commands/idl_publish.py +80 -0
- pythsol/cli/commands/init.py +65 -0
- pythsol/cli/commands/port_cmd.py +42 -0
- pythsol/cli/commands/profile.py +57 -0
- pythsol/cli/commands/serve.py +52 -0
- pythsol/cli/commands/test_cmd.py +217 -0
- pythsol/cli/commands/verify.py +96 -0
- pythsol/cli/commands/wallet.py +128 -0
- pythsol/cli/config.py +84 -0
- pythsol/cli/main.py +72 -0
- pythsol/cli/rpc.py +56 -0
- pythsol/cli/template_programs/__init__.py +1 -0
- pythsol/cli/template_programs/counter.py +52 -0
- pythsol/cli/template_programs/escrow.py +75 -0
- pythsol/cli/template_programs/ledger.py +68 -0
- pythsol/cli/template_programs/multisig.py +91 -0
- pythsol/cli/template_programs/poll.py +70 -0
- pythsol/cli/template_programs/vault.py +67 -0
- pythsol/cli/templates.py +192 -0
- pythsol/client/__init__.py +5 -0
- pythsol/client/from_idl.py +356 -0
- pythsol/client/gen.py +496 -0
- pythsol/client/port.py +355 -0
- pythsol/compiler/__init__.py +39 -0
- pythsol/compiler/diagnostics.py +130 -0
- pythsol/compiler/emit.py +3550 -0
- pythsol/compiler/front.py +84 -0
- pythsol/compiler/idl.py +465 -0
- pythsol/compiler/model.py +1329 -0
- pythsol/compiler/modules.py +320 -0
- pythsol/compiler/parse.py +59 -0
- pythsol/compiler/pipeline.py +152 -0
- pythsol/compiler/resolve.py +518 -0
- pythsol/compiler/subset.py +1061 -0
- pythsol/compiler/toolchain.py +438 -0
- pythsol/compiler/typecheck.py +2447 -0
- pythsol/playground/__init__.py +7 -0
- pythsol/playground/app.css +182 -0
- pythsol/playground/app.js +260 -0
- pythsol/playground/apple-touch-icon.png +0 -0
- pythsol/playground/docs.html +61 -0
- pythsol/playground/favicon-32.png +0 -0
- pythsol/playground/favicon.ico +0 -0
- pythsol/playground/favicon.svg +24 -0
- pythsol/playground/hero-bg.webp +0 -0
- pythsol/playground/index.html +66 -0
- pythsol/playground/landing.html +810 -0
- pythsol/playground/playground.html +77 -0
- pythsol/playground/robots.txt +4 -0
- pythsol/playground/server.py +336 -0
- pythsol/playground/site-bg.svg +146 -0
- pythsol/playground/site.css +1105 -0
- pythsol/py.typed +0 -0
- pythsol/refrt/__init__.py +200 -0
- pythsol/runtime/__init__.py +17 -0
- pythsol/runtime/pythsol_rt.h +1216 -0
- pythsol/testing/__init__.py +12 -0
- pythsol/testing/plugin.py +437 -0
- pythsol-0.1.0.dist-info/METADATA +178 -0
- pythsol-0.1.0.dist-info/RECORD +76 -0
- pythsol-0.1.0.dist-info/WHEEL +4 -0
- pythsol-0.1.0.dist-info/entry_points.txt +6 -0
- 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
|