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
|
@@ -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)
|
pythsol/cli/templates.py
ADDED
|
@@ -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,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)
|