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
@@ -0,0 +1,67 @@
1
+ """Vault: a PDA that holds lamports for one authority.
2
+
3
+ ``deposit`` moves lamports in with ``system_transfer``. ``close`` checks the
4
+ authority, then returns every lamport on the account. The PDA seed is the
5
+ literal ``vault`` (one seed, plus the bump).
6
+ """
7
+
8
+ from pythsol import (
9
+ CloseTo,
10
+ HasOne,
11
+ Init,
12
+ Mut,
13
+ ProgramError,
14
+ Pubkey,
15
+ Seeds,
16
+ Signer,
17
+ account,
18
+ error,
19
+ instruction,
20
+ require,
21
+ system_transfer,
22
+ u8,
23
+ u64,
24
+ )
25
+
26
+
27
+ @account
28
+ class Vault:
29
+ authority: Pubkey
30
+ balance: u64
31
+ bump: u8
32
+
33
+
34
+ @error
35
+ class VaultError(ProgramError):
36
+ """Errors returned by this program (6000 + index)."""
37
+
38
+ Unauthorized = 100
39
+
40
+
41
+ @instruction
42
+ def initialize(
43
+ authority: Signer,
44
+ vault: Seeds[Init[Vault], "vault"], # type: ignore[name-defined] # noqa: F821
45
+ bump: u8,
46
+ ) -> None:
47
+ vault.authority = authority.key
48
+ vault.balance = 0
49
+ vault.bump = bump
50
+
51
+
52
+ @instruction
53
+ def deposit(
54
+ authority: Signer,
55
+ vault: HasOne[Mut[Vault], "authority"], # type: ignore[name-defined] # noqa: F821
56
+ amount: u64,
57
+ ) -> None:
58
+ system_transfer(authority, vault, amount)
59
+ vault.balance += amount
60
+
61
+
62
+ @instruction
63
+ def close(
64
+ authority: Signer,
65
+ vault: CloseTo[Mut[Vault], authority], # type: ignore[name-defined] # noqa: F821
66
+ ) -> None:
67
+ require(vault.authority == authority.key, VaultError.Unauthorized)
@@ -0,0 +1,192 @@
1
+ """Project files written by `pythsol init`.
2
+
3
+ Program sources live in :mod:`pythsol.cli.template_programs` so a scaffold
4
+ does not depend on the ``examples/`` tree being installed.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from importlib.resources import files
10
+
11
+ __all__ = ["TEMPLATES", "program_source", "program_test_source", "project_files"]
12
+
13
+ TEMPLATES: tuple[str, ...] = ("counter", "ledger", "vault", "escrow", "multisig", "poll")
14
+
15
+ _PYSOL = """\
16
+ [project]
17
+ name = "{name}"
18
+ python = ">=3.11"
19
+
20
+ [[programs]]
21
+ name = "{name}"
22
+ entry = "programs/{name}.py"
23
+
24
+ [build]
25
+ sbpf = "v0"
26
+ heap_kib = 32
27
+ out_dir = "target/pythsol"
28
+ """
29
+
30
+ _PYPROJECT = """\
31
+ [project]
32
+ name = "{name}"
33
+ version = "0.1.0"
34
+ description = "Solana program written with PythSol"
35
+ requires-python = ">=3.11"
36
+ dependencies = ["pythsol"]
37
+ """
38
+
39
+ _README = """\
40
+ # {name}
41
+
42
+ PythSol program. From this directory:
43
+
44
+ ```bash
45
+ pythsol check programs/{name}.py
46
+ pythsol build programs/{name}.py
47
+ pythsol idl programs/{name}.py
48
+ ```
49
+
50
+ `pythsol build` writes a native SBF `.so`, an Anchor-style IDL, and
51
+ `{name}_client.py` (solders instruction builders). `pythsol test` builds
52
+ again (a cache hit skips clang) and runs `tests/` on LiteSVM.
53
+ """
54
+
55
+
56
+ def program_source(template: str) -> str:
57
+ """Return the embedded program source for ``template``."""
58
+ resource = files("pythsol.cli.template_programs").joinpath(f"{template}.py")
59
+ return resource.read_text(encoding="utf-8")
60
+
61
+
62
+ _COUNTER_TEST = """\
63
+ def test_increment(program, payer):
64
+ opened = program.initialize(authority=payer)
65
+ counter = opened.account("counter")
66
+ program.increment(authority=payer, counter=counter, by=5)
67
+ state = program.fetch("Counter", counter)
68
+ assert state["count"] == 5
69
+
70
+
71
+ def test_unauthorized(program, payer):
72
+ opened = program.initialize(authority=payer)
73
+ counter = opened.account("counter")
74
+ stranger = program.keypair()
75
+ with program.expect_error("Unauthorized"):
76
+ program.increment(authority=stranger, counter=counter, by=1)
77
+ """
78
+
79
+ _LEDGER_TEST = """\
80
+ def test_deposit(program, payer):
81
+ opened = program.initialize(authority=payer)
82
+ ledger = opened.account("ledger")
83
+ program.deposit(authority=payer, ledger=ledger, slot=1, amount=5)
84
+ state = program.fetch("Ledger", ledger)
85
+ assert state["total"] == 5
86
+ """
87
+
88
+ _VAULT_TEST = """\
89
+ def test_deposit_and_close(program, payer):
90
+ opened = program.initialize(authority=payer)
91
+ vault = opened.account("vault")
92
+ program.deposit(authority=payer, vault=vault, amount=1000)
93
+ state = program.fetch("Vault", vault)
94
+ assert state["balance"] == 1000
95
+ program.close(authority=payer, vault=vault)
96
+ """
97
+
98
+ _ESCROW_TEST = """\
99
+ def test_make_and_take(program, payer):
100
+ taker = program.keypair()
101
+ opened = program.make(maker=payer, taker=taker, amount=1000)
102
+ escrow = opened.account("escrow")
103
+ program.take(taker=taker, maker=payer, escrow=escrow, price=400)
104
+ """
105
+
106
+ _MULTISIG_TEST = """\
107
+ def test_two_of_three(program, payer):
108
+ first = program.keypair()
109
+ second = program.keypair()
110
+ third = program.keypair()
111
+ dest = program.keypair()
112
+ opened = program.initialize(
113
+ authority=payer,
114
+ first=first.pubkey(),
115
+ second=second.pubkey(),
116
+ third=third.pubkey(),
117
+ threshold=2,
118
+ )
119
+ treasury = opened.account("treasury")
120
+ program.fund(donor=payer, treasury=treasury, amount=1000)
121
+ program.approve(owner=first, treasury=treasury, slot=0)
122
+ program.approve(owner=second, treasury=treasury, slot=1)
123
+ program.release(owner=second, treasury=treasury, dest=dest, slot=1, amount=400)
124
+ assert program.fetch("Treasury", treasury)["paid"] == 400
125
+
126
+
127
+ def test_one_approval_is_not_enough(program, payer):
128
+ first = program.keypair()
129
+ second = program.keypair()
130
+ third = program.keypair()
131
+ dest = program.keypair()
132
+ opened = program.initialize(
133
+ authority=payer,
134
+ first=first.pubkey(),
135
+ second=second.pubkey(),
136
+ third=third.pubkey(),
137
+ threshold=2,
138
+ )
139
+ treasury = opened.account("treasury")
140
+ program.fund(donor=payer, treasury=treasury, amount=1000)
141
+ program.approve(owner=first, treasury=treasury, slot=0)
142
+ with program.expect_error("NotApproved"):
143
+ program.release(owner=second, treasury=treasury, dest=dest, slot=1, amount=400)
144
+ """
145
+
146
+ _POLL_TEST = """\
147
+ def test_one_vote_per_key(program, payer):
148
+ voters = [program.keypair() for _ in range(4)]
149
+ opened = program.initialize(
150
+ authority=payer,
151
+ first=voters[0].pubkey(),
152
+ second=voters[1].pubkey(),
153
+ third=voters[2].pubkey(),
154
+ fourth=voters[3].pubkey(),
155
+ )
156
+ poll = opened.account("poll")
157
+ program.vote(voter=voters[0], poll=poll, slot=0, choice=True)
158
+ program.vote(voter=voters[3], poll=poll, slot=3, choice=False)
159
+ state = program.fetch("Poll", poll)
160
+ assert state["yes"] == 1
161
+ assert state["no"] == 1
162
+ with program.expect_error("AlreadyVoted"):
163
+ program.vote(voter=voters[0], poll=poll, slot=0, choice=True)
164
+ """
165
+
166
+ _TEMPLATE_TESTS = {
167
+ "counter": _COUNTER_TEST,
168
+ "ledger": _LEDGER_TEST,
169
+ "vault": _VAULT_TEST,
170
+ "escrow": _ESCROW_TEST,
171
+ "multisig": _MULTISIG_TEST,
172
+ "poll": _POLL_TEST,
173
+ }
174
+
175
+
176
+ def program_test_source(template: str) -> str:
177
+ """Pytest source for ``template``. It uses plugin fixtures only."""
178
+ try:
179
+ return _TEMPLATE_TESTS[template]
180
+ except KeyError as exc:
181
+ raise ValueError(f"unknown template {template}") from exc
182
+
183
+
184
+ def project_files(name: str, template: str) -> dict[str, str]:
185
+ """Relative path → file text for a new project."""
186
+ return {
187
+ "pysol.toml": _PYSOL.format(name=name),
188
+ "pyproject.toml": _PYPROJECT.format(name=name),
189
+ "README.md": _README.format(name=name),
190
+ f"programs/{name}.py": program_source(template),
191
+ "tests/test_program.py": program_test_source(template),
192
+ }
@@ -0,0 +1,5 @@
1
+ """Python client generation (solders instruction builders and account decoders)."""
2
+
3
+ from pythsol.client.gen import render_client
4
+
5
+ __all__ = ["render_client"]
@@ -0,0 +1,356 @@
1
+ """Render a solders client from an Anchor-legacy IDL document.
2
+
3
+ ``pythsol build`` still uses :func:`pythsol.client.gen.render_client`, which
4
+ sees the program model. This path is for ``pythsol client program.json``,
5
+ where the only input is the IDL. CPI programs to append live under
6
+ ``metadata.pythsol.cpi`` (written by :func:`pythsol.compiler.idl.build_idl`).
7
+ The generated module imports solders, not the compiler.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import keyword
13
+ from typing import Any
14
+
15
+ __all__ = ["render_client_from_idl"]
16
+
17
+ _SYSTEM = "11111111111111111111111111111111"
18
+ _TOKEN = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
19
+ _TOKEN_2022 = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"
20
+ _ATA = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
21
+
22
+ _FMT = {
23
+ "u8": "<B",
24
+ "u16": "<H",
25
+ "u32": "<I",
26
+ "u64": "<Q",
27
+ "i8": "<b",
28
+ "i16": "<h",
29
+ "i32": "<i",
30
+ "i64": "<q",
31
+ "bool": "<B",
32
+ }
33
+
34
+ _WIDTH = {
35
+ "u8": 1,
36
+ "u16": 2,
37
+ "u32": 4,
38
+ "u64": 8,
39
+ "u128": 16,
40
+ "i8": 1,
41
+ "i16": 2,
42
+ "i32": 4,
43
+ "i64": 8,
44
+ "i128": 16,
45
+ "bool": 1,
46
+ "publicKey": 32,
47
+ }
48
+
49
+ _EMPTY_CPI = {
50
+ "ata": False,
51
+ "pays": False,
52
+ "system": False,
53
+ "token": False,
54
+ "token2022": False,
55
+ }
56
+
57
+
58
+ class IdlClientError(Exception):
59
+ """The IDL uses a type this renderer does not pack."""
60
+
61
+
62
+ def render_client_from_idl(idl: dict[str, Any], *, address: str | None = None) -> str:
63
+ """Return ``{name}_client.py`` source for ``idl``."""
64
+ name = str(idl.get("name") or "program")
65
+ flags = [_cpi(idl, str(item.get("name") or "")) for item in idl.get("instructions") or []]
66
+ needs_token = any(item["token"] or item["ata"] for item in flags)
67
+ needs_token2022 = any(item["token2022"] for item in flags)
68
+ needs_ata = any(item["ata"] for item in flags)
69
+ lines = [
70
+ '"""Generated by PythSol from an IDL. Do not edit.',
71
+ "",
72
+ f"Program `{name}`.",
73
+ "Build transactions with solders. This module does not import the compiler.",
74
+ '"""',
75
+ "",
76
+ "from __future__ import annotations",
77
+ "",
78
+ "import struct",
79
+ "",
80
+ "from solders.instruction import AccountMeta, Instruction",
81
+ "from solders.pubkey import Pubkey",
82
+ "",
83
+ f"PROGRAM_NAME = {name!r}",
84
+ ]
85
+ if address:
86
+ lines.append(f"PROGRAM_ID = {address!r}")
87
+ lines.append(f'_SYSTEM_PROGRAM = Pubkey.from_string("{_SYSTEM}")')
88
+ if needs_token:
89
+ lines.append(f'_TOKEN_PROGRAM = Pubkey.from_string("{_TOKEN}")')
90
+ if needs_token2022:
91
+ lines.append(f'_TOKEN_2022_PROGRAM = Pubkey.from_string("{_TOKEN_2022}")')
92
+ if needs_ata:
93
+ lines.append(f'_ATA_PROGRAM = Pubkey.from_string("{_ATA}")')
94
+ lines.append("")
95
+ types = _types(idl)
96
+ lines.extend(_enum_constants(types))
97
+ for instr in idl.get("instructions") or []:
98
+ if isinstance(instr, dict):
99
+ lines.extend(_instruction(instr, _cpi(idl, str(instr.get("name") or "")), types))
100
+ lines.append("")
101
+ for account in idl.get("accounts") or []:
102
+ if isinstance(account, dict):
103
+ lines.extend(_decode_account(account, types))
104
+ lines.append("")
105
+ return "\n".join(lines).rstrip() + "\n"
106
+
107
+
108
+ def _types(idl: dict[str, Any]) -> dict[str, dict[str, Any]]:
109
+ found: dict[str, dict[str, Any]] = {}
110
+ for item in idl.get("types") or []:
111
+ if isinstance(item, dict) and isinstance(item.get("name"), str):
112
+ found[item["name"]] = item
113
+ return found
114
+
115
+
116
+ def _cpi(idl: dict[str, Any], name: str) -> dict[str, bool]:
117
+ metadata = idl.get("metadata")
118
+ if not isinstance(metadata, dict):
119
+ return dict(_EMPTY_CPI)
120
+ pythsol = metadata.get("pythsol")
121
+ if not isinstance(pythsol, dict):
122
+ return dict(_EMPTY_CPI)
123
+ table = pythsol.get("cpi")
124
+ if not isinstance(table, dict):
125
+ return dict(_EMPTY_CPI)
126
+ raw = table.get(name)
127
+ if not isinstance(raw, dict):
128
+ return dict(_EMPTY_CPI)
129
+ return {key: bool(raw.get(key)) for key in _EMPTY_CPI}
130
+
131
+
132
+ def _enum_constants(types: dict[str, dict[str, Any]]) -> list[str]:
133
+ lines: list[str] = []
134
+ for name, info in types.items():
135
+ body = info.get("type")
136
+ if not isinstance(body, dict) or body.get("kind") != "enum":
137
+ continue
138
+ variants = body.get("variants")
139
+ if not isinstance(variants, list):
140
+ continue
141
+ if not lines:
142
+ lines.append("# Unit enum tags (declaration index, one byte).")
143
+ for index, variant in enumerate(variants):
144
+ if isinstance(variant, dict) and isinstance(variant.get("name"), str):
145
+ const = f"{name.upper()}_{variant['name'].upper()}"
146
+ lines.append(f"{const} = {index} # {name}.{variant['name']}")
147
+ if lines:
148
+ lines.append("")
149
+ return lines
150
+
151
+
152
+ def _fn_name(name: str, used: set[str]) -> str:
153
+ candidate = name + "_" if keyword.iskeyword(name) else name
154
+ while candidate in used:
155
+ candidate += "_"
156
+ used.add(candidate)
157
+ return candidate
158
+
159
+
160
+ def _py_type(ty: Any, types: dict[str, dict[str, Any]]) -> str:
161
+ if ty == "publicKey":
162
+ return "Pubkey"
163
+ if ty == "bool":
164
+ return "bool"
165
+ if isinstance(ty, str):
166
+ return "int"
167
+ if isinstance(ty, dict) and ty.get("defined"):
168
+ info = types.get(str(ty["defined"]))
169
+ body = info.get("type") if isinstance(info, dict) else None
170
+ if isinstance(body, dict) and body.get("kind") == "enum":
171
+ return "int"
172
+ return "object"
173
+ return "object"
174
+
175
+
176
+ def _pack_expr(ty: Any, name: str, types: dict[str, dict[str, Any]]) -> str:
177
+ if ty == "publicKey":
178
+ return f"bytes({name})"
179
+ if ty == "bool":
180
+ return f'struct.pack("<B", 1 if {name} else 0)'
181
+ if ty == "u128":
182
+ return f'{name}.to_bytes(16, "little")'
183
+ if ty == "i128":
184
+ return f'{name}.to_bytes(16, "little", signed=True)'
185
+ if isinstance(ty, str) and ty in _FMT:
186
+ return f'struct.pack("{_FMT[ty]}", {name})'
187
+ if isinstance(ty, dict) and isinstance(ty.get("defined"), str):
188
+ info = types.get(ty["defined"])
189
+ body = info.get("type") if isinstance(info, dict) else None
190
+ if isinstance(body, dict) and body.get("kind") == "enum":
191
+ return f'struct.pack("<B", {name})'
192
+ raise IdlClientError(f"cannot pack IDL type {ty!r}")
193
+
194
+
195
+ def _instruction(
196
+ instr: dict[str, Any],
197
+ cpi: dict[str, bool],
198
+ types: dict[str, dict[str, Any]],
199
+ ) -> list[str]:
200
+ raw_name = str(instr.get("name") or "ix")
201
+ disc = instr.get("discriminator")
202
+ if not isinstance(disc, list) or len(disc) != 8:
203
+ raise IdlClientError(f"instruction `{raw_name}` needs an 8-byte discriminator")
204
+ const = f"_DISC_{raw_name.upper()}"
205
+ inner = ", ".join(str(int(byte) & 0xFF) for byte in disc)
206
+ used = {"program_id"}
207
+ params = ["program_id: Pubkey"]
208
+ account_lines: list[str] = []
209
+ raw_accounts = instr.get("accounts")
210
+ accounts = raw_accounts if isinstance(raw_accounts, list) else []
211
+ for account in accounts:
212
+ if not isinstance(account, dict):
213
+ continue
214
+ arg = _fn_name(str(account.get("name") or "account"), used)
215
+ params.append(f"{arg}: Pubkey")
216
+ signer = bool(account.get("isSigner"))
217
+ writable = bool(account.get("isMut")) or (signer and cpi["pays"])
218
+ account_lines.append(
219
+ f" AccountMeta({arg}, is_signer={signer}, is_writable={writable}),"
220
+ )
221
+ data_exprs = [const]
222
+ raw_args = instr.get("args")
223
+ args = raw_args if isinstance(raw_args, list) else []
224
+ for arg in args:
225
+ if not isinstance(arg, dict):
226
+ continue
227
+ arg_name = _fn_name(str(arg.get("name") or "arg"), used)
228
+ params.append(f"{arg_name}: {_py_type(arg.get('type'), types)}")
229
+ data_exprs.append(_pack_expr(arg.get("type"), arg_name, types))
230
+ if cpi["system"]:
231
+ account_lines.append(
232
+ " AccountMeta(_SYSTEM_PROGRAM, is_signer=False, is_writable=False),"
233
+ )
234
+ if cpi["token"] or cpi["ata"]:
235
+ account_lines.append(
236
+ " AccountMeta(_TOKEN_PROGRAM, is_signer=False, is_writable=False),"
237
+ )
238
+ if cpi["token2022"]:
239
+ account_lines.append(
240
+ " AccountMeta(_TOKEN_2022_PROGRAM, is_signer=False, is_writable=False),"
241
+ )
242
+ if cpi["ata"]:
243
+ account_lines.append(
244
+ " AccountMeta(_ATA_PROGRAM, is_signer=False, is_writable=False),"
245
+ )
246
+ header = f"def {raw_name}(" + ", ".join(params) + ") -> Instruction:"
247
+ lines = [f"{const} = bytes(({inner}))", ""]
248
+ if len(header) <= 100 and "\n" not in header:
249
+ lines.append(header)
250
+ else:
251
+ lines.append(f"def {raw_name}(")
252
+ for param in params:
253
+ lines.append(f" {param},")
254
+ lines.append(") -> Instruction:")
255
+ lines.append(f' """Build the `{raw_name}` instruction."""')
256
+ if len(data_exprs) == 1:
257
+ lines.append(f" data = {const}")
258
+ else:
259
+ lines.append(" data = b''.join((")
260
+ for expr in data_exprs:
261
+ lines.append(f" {expr},")
262
+ lines.append(" ))")
263
+ lines.append(" accounts = [")
264
+ lines.extend(account_lines)
265
+ lines.append(" ]")
266
+ lines.append(" return Instruction(program_id=program_id, data=data, accounts=accounts)")
267
+ return lines
268
+
269
+
270
+ def _simple_width(ty: Any, types: dict[str, dict[str, Any]]) -> int | None:
271
+ if isinstance(ty, str):
272
+ return _WIDTH.get(ty)
273
+ if isinstance(ty, dict) and isinstance(ty.get("defined"), str):
274
+ info = types.get(ty["defined"])
275
+ body = info.get("type") if isinstance(info, dict) else None
276
+ if isinstance(body, dict) and body.get("kind") == "enum":
277
+ return 1
278
+ if isinstance(ty, dict) and isinstance(ty.get("array"), list) and len(ty["array"]) == 2:
279
+ inner, count = ty["array"]
280
+ width = _simple_width(inner, types)
281
+ if width is None or not isinstance(count, int):
282
+ return None
283
+ return width * count
284
+ return None
285
+
286
+
287
+ def _read_simple(name: str, ty: Any, offset: int, types: dict[str, dict[str, Any]]) -> str:
288
+ if ty == "publicKey":
289
+ return f" {name} = Pubkey.from_bytes(data[{offset}:{offset + 32}])"
290
+ if ty == "bool":
291
+ return f" {name} = bool(data[{offset}])"
292
+ if ty == "u128":
293
+ return f' {name} = int.from_bytes(data[{offset}:{offset + 16}], "little")'
294
+ if ty == "i128":
295
+ return f' {name} = int.from_bytes(data[{offset}:{offset + 16}], "little", signed=True)'
296
+ if isinstance(ty, str) and ty in _FMT:
297
+ if ty == "u8":
298
+ return f" {name} = data[{offset}]"
299
+ return f' ({name},) = struct.unpack_from("{_FMT[ty]}", data, {offset})'
300
+ if isinstance(ty, dict) and isinstance(ty.get("defined"), str):
301
+ return f" {name} = data[{offset}]"
302
+ width = _simple_width(ty, types)
303
+ if width is None:
304
+ raise IdlClientError(f"cannot read IDL type {ty!r}")
305
+ return f" {name} = data[{offset}:{offset + width}]"
306
+
307
+
308
+ def _decode_account(account: dict[str, Any], types: dict[str, dict[str, Any]]) -> list[str]:
309
+ name = str(account.get("name") or "Account")
310
+ fn = "decode_" + _snake(name)
311
+ body = account.get("type") if isinstance(account.get("type"), dict) else {}
312
+ fields = body.get("fields") if isinstance(body, dict) else None
313
+ if not isinstance(fields, list):
314
+ fields = []
315
+ offset = 8
316
+ reads: list[str] = []
317
+ simple = True
318
+ for field in fields:
319
+ if not isinstance(field, dict):
320
+ simple = False
321
+ break
322
+ width = _simple_width(field.get("type"), types)
323
+ if width is None:
324
+ simple = False
325
+ break
326
+ field_name = str(field.get("name") or "field")
327
+ if isinstance(field.get("type"), dict) and isinstance(field["type"].get("array"), list):
328
+ simple = False
329
+ break
330
+ reads.append(_read_simple(field_name, field.get("type"), offset, types))
331
+ offset += width
332
+ lines = [
333
+ f"def {fn}(data: bytes) -> dict[str, object]:",
334
+ f' """Decode a `{name}` account (discriminator plus fields)."""',
335
+ ]
336
+ if not simple:
337
+ lines.append(f' raise NotImplementedError("decode `{name}` from the program source")')
338
+ return lines
339
+ lines.append(f" if len(data) < {offset}:")
340
+ lines.append(f' raise ValueError("{name} account is shorter than {offset} bytes")')
341
+ lines.extend(reads)
342
+ lines.append(" return {")
343
+ for field in fields:
344
+ if isinstance(field, dict):
345
+ lines.append(f" {str(field.get('name') or 'field')!r}: {field.get('name')},")
346
+ lines.append(" }")
347
+ return lines
348
+
349
+
350
+ def _snake(name: str) -> str:
351
+ out: list[str] = []
352
+ for char in name:
353
+ if char.isupper() and out and out[-1] != "_":
354
+ out.append("_")
355
+ out.append(char.lower())
356
+ return "".join(out)