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/__init__.py
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"""Typed DSL surface that programs import from.
|
|
2
|
+
|
|
3
|
+
This file is intentionally *importable only as types*: the compiler parses the
|
|
4
|
+
import with `ast` and never executes it, while mypy/pyright use it to
|
|
5
|
+
type-check programs (`py.typed` marker lives beside the package). Phase 3 adds
|
|
6
|
+
the actual runtime semantics — this slice is what Phase 2 accepts.
|
|
7
|
+
|
|
8
|
+
Every program may import:
|
|
9
|
+
* width types: u8,u16,u32,u64,u128 and i8..i128
|
|
10
|
+
* atoms: bool, Pubkey, Signer, SystemProgram
|
|
11
|
+
* wrappers: Mut[T], Init[T], InitIfNeeded[T], Zeroed[T], Account[T]
|
|
12
|
+
* collections: Option[T], Vec[T], FixedArray[T, N], FixedString[N]
|
|
13
|
+
* decorators: @instruction, @account, @struct, @event, @error, @enum
|
|
14
|
+
* accounts: TokenAccount (writable), TokenMint (read-only), no owner check
|
|
15
|
+
* helpers: require, emit, system_transfer, token_transfer,
|
|
16
|
+
token2022_transfer, ata_create
|
|
17
|
+
* `ProgramError` base class
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from typing import Any, Generic, TypeAlias, TypeVar
|
|
23
|
+
|
|
24
|
+
__version__ = "0.1.0"
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"Account",
|
|
28
|
+
"Address",
|
|
29
|
+
"CloseTo",
|
|
30
|
+
"FixedArray",
|
|
31
|
+
"FixedString",
|
|
32
|
+
"HasOne",
|
|
33
|
+
"Init",
|
|
34
|
+
"InitIfNeeded",
|
|
35
|
+
"Mut",
|
|
36
|
+
"Option",
|
|
37
|
+
"Owner",
|
|
38
|
+
"ProgramError",
|
|
39
|
+
"Pubkey",
|
|
40
|
+
"Seeds",
|
|
41
|
+
"Signer",
|
|
42
|
+
"SystemProgram",
|
|
43
|
+
"TokenAccount",
|
|
44
|
+
"TokenMint",
|
|
45
|
+
"Vec",
|
|
46
|
+
"Zeroed",
|
|
47
|
+
"account",
|
|
48
|
+
"ata_create",
|
|
49
|
+
"emit",
|
|
50
|
+
"enum",
|
|
51
|
+
"error",
|
|
52
|
+
"event",
|
|
53
|
+
"i8",
|
|
54
|
+
"i16",
|
|
55
|
+
"i32",
|
|
56
|
+
"i64",
|
|
57
|
+
"i128",
|
|
58
|
+
"instruction",
|
|
59
|
+
"require",
|
|
60
|
+
"struct",
|
|
61
|
+
"system_transfer",
|
|
62
|
+
"token2022_transfer",
|
|
63
|
+
"token_transfer",
|
|
64
|
+
"u8",
|
|
65
|
+
"u16",
|
|
66
|
+
"u32",
|
|
67
|
+
"u64",
|
|
68
|
+
"u128",
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
_T = TypeVar("_T")
|
|
72
|
+
_N = TypeVar("_N")
|
|
73
|
+
|
|
74
|
+
# widths ---------------------------------------------------------------
|
|
75
|
+
u8: TypeAlias = int
|
|
76
|
+
u16: TypeAlias = int
|
|
77
|
+
u32: TypeAlias = int
|
|
78
|
+
u64: TypeAlias = int
|
|
79
|
+
u128: TypeAlias = int
|
|
80
|
+
i8: TypeAlias = int
|
|
81
|
+
i16: TypeAlias = int
|
|
82
|
+
i32: TypeAlias = int
|
|
83
|
+
i64: TypeAlias = int
|
|
84
|
+
i128: TypeAlias = int
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# atoms ----------------------------------------------------------------
|
|
88
|
+
class Pubkey:
|
|
89
|
+
"""An opaque 32-byte on-chain identity."""
|
|
90
|
+
|
|
91
|
+
key: Pubkey
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class Signer(Pubkey):
|
|
95
|
+
"""Binding contract: this account must be a required signer."""
|
|
96
|
+
|
|
97
|
+
key: Pubkey
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class SystemProgram:
|
|
101
|
+
"""Anchor `system_program` stand-in used as a `@account Permissioned type`."""
|
|
102
|
+
|
|
103
|
+
program_id: Pubkey
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class TokenAccount:
|
|
107
|
+
"""A writable SPL token account. The program does not check its owner."""
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class TokenMint:
|
|
111
|
+
"""A read-only mint. The program does not check its owner."""
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class ProgramError:
|
|
115
|
+
"""Base class of `@error` program errors."""
|
|
116
|
+
|
|
117
|
+
message: str | None = None
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# wrappers -------------------------------------------------------------
|
|
121
|
+
class Mut(Generic[_T]):
|
|
122
|
+
"""Flag: this instruction can write to the wrapped type."""
|
|
123
|
+
|
|
124
|
+
def __getattribute__(self, name: str) -> Any: # pragma: no cover
|
|
125
|
+
...
|
|
126
|
+
|
|
127
|
+
def __setattr__(self, name: str, value: Any) -> None: # pragma: no cover
|
|
128
|
+
...
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class Init(Mut[_T]):
|
|
132
|
+
"""Marks this instruction as the one that initializes the account."""
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class InitIfNeeded(Mut[_T]):
|
|
136
|
+
"""New-account-or-existing? Runtime-conditional."""
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class Zeroed(Mut[_T]):
|
|
140
|
+
"""The account must be zeroed before this instruction runs."""
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class Account(Generic[_T]):
|
|
144
|
+
"""Read-only account wrapper."""
|
|
145
|
+
|
|
146
|
+
def __getattribute__(self, name: str) -> Any: # pragma: no cover
|
|
147
|
+
...
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class Seeds(Generic[_T, _N]):
|
|
151
|
+
"""``Seeds[Init[T], "seed"]`` — the PDA seed, instead of the account name.
|
|
152
|
+
|
|
153
|
+
The second argument is a string literal. Type checkers treat that literal
|
|
154
|
+
as a forward reference, so a program line carries ``type: ignore`` and
|
|
155
|
+
``noqa: F821``. Attribute access is untyped, like ``Mut``.
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
def __getattribute__(self, name: str) -> Any: # pragma: no cover
|
|
159
|
+
...
|
|
160
|
+
|
|
161
|
+
def __setattr__(self, name: str, value: Any) -> None: # pragma: no cover
|
|
162
|
+
...
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class HasOne(Generic[_T, _N]):
|
|
166
|
+
"""``HasOne[Mut[T], "field"]`` — ``account.field`` equals that account's key."""
|
|
167
|
+
|
|
168
|
+
def __getattribute__(self, name: str) -> Any: # pragma: no cover
|
|
169
|
+
...
|
|
170
|
+
|
|
171
|
+
def __setattr__(self, name: str, value: Any) -> None: # pragma: no cover
|
|
172
|
+
...
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class CloseTo(Generic[_T, _N]):
|
|
176
|
+
"""``CloseTo[Mut[T], dest]`` — after a successful body, lamports move to ``dest``."""
|
|
177
|
+
|
|
178
|
+
def __getattribute__(self, name: str) -> Any: # pragma: no cover
|
|
179
|
+
...
|
|
180
|
+
|
|
181
|
+
def __setattr__(self, name: str, value: Any) -> None: # pragma: no cover
|
|
182
|
+
...
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class Address(Generic[_T, _N]):
|
|
186
|
+
"""``Address[Account[T], expected]`` — the account key equals a ``Pubkey`` argument."""
|
|
187
|
+
|
|
188
|
+
def __getattribute__(self, name: str) -> Any: # pragma: no cover
|
|
189
|
+
...
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class Owner(Generic[_T, _N]):
|
|
193
|
+
"""``Owner[Account[T], program]`` — ``account.owner`` equals that account's key."""
|
|
194
|
+
|
|
195
|
+
def __getattribute__(self, name: str) -> Any: # pragma: no cover
|
|
196
|
+
...
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# collections ----------------------------------------------------------
|
|
200
|
+
class Option(Generic[_T]):
|
|
201
|
+
"""Fixed-width optional value: a tag byte plus the inner value, always reserved.
|
|
202
|
+
|
|
203
|
+
``None`` clears the tag. Assigning a ``T`` sets it. ``.is_some`` reads the
|
|
204
|
+
tag; ``.value`` reads the inner value and aborts when the tag is clear.
|
|
205
|
+
This is not Borsh's short ``None`` (one zero byte). Later fields stay put.
|
|
206
|
+
"""
|
|
207
|
+
|
|
208
|
+
def __getattribute__(self, name: str) -> Any: # pragma: no cover
|
|
209
|
+
...
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class Vec(Generic[_T]):
|
|
213
|
+
"""Account-resizing vector (Anchor `Vec[T]`)."""
|
|
214
|
+
|
|
215
|
+
def __getattribute__(self, name: str) -> Any: # pragma: no cover
|
|
216
|
+
...
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class FixedArray(Generic[_T, _N]):
|
|
220
|
+
"""Fixed-length typed array. ``N`` in ``FixedArray[T, N]`` is an integer literal."""
|
|
221
|
+
|
|
222
|
+
def __getattribute__(self, name: str) -> Any: # pragma: no cover
|
|
223
|
+
...
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class FixedString(Generic[_N]):
|
|
227
|
+
"""Fixed-width string: a ``u32`` byte length plus ``N`` reserved bytes.
|
|
228
|
+
|
|
229
|
+
Store a string literal whose UTF-8 form fits in ``N``. ``len`` reads the
|
|
230
|
+
stored byte count. This is not Borsh's short string: the ``N`` bytes stay
|
|
231
|
+
reserved so later fields do not move.
|
|
232
|
+
"""
|
|
233
|
+
|
|
234
|
+
def __getattribute__(self, name: str) -> Any: # pragma: no cover
|
|
235
|
+
...
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
# decorators -----------------------------------------------------------
|
|
239
|
+
def instruction(func: Any) -> Any:
|
|
240
|
+
"""Mark a function as an instruction entry point."""
|
|
241
|
+
return func
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def account(value: Any) -> Any:
|
|
245
|
+
"""Mark a class as a program-local account layout."""
|
|
246
|
+
return value
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def error(value: Any) -> Any:
|
|
250
|
+
"""Mark a class as an on-chain error."""
|
|
251
|
+
return value
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def enum(value: Any) -> Any:
|
|
255
|
+
"""Mark a class as a one-byte unit enum (``Name = <declaration index>``)."""
|
|
256
|
+
return value
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def struct(value: Any) -> Any:
|
|
260
|
+
"""Mark a class as a fixed-layout value nested inside an account."""
|
|
261
|
+
return value
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def event(value: Any) -> Any:
|
|
265
|
+
"""Mark a class as a fixed-width value logged by ``emit``."""
|
|
266
|
+
return value
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def token_transfer(src: object, dest: object, authority: object, amount: int) -> None:
|
|
270
|
+
"""SPL Token Transfer (instruction 3) from ``src`` to ``dest``."""
|
|
271
|
+
del src, dest, authority, amount
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def token2022_transfer(src: object, dest: object, authority: object, amount: int) -> None:
|
|
275
|
+
"""Token-2022 Transfer (instruction 3). Same accounts as ``token_transfer``."""
|
|
276
|
+
del src, dest, authority, amount
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def ata_create(payer: object, ata: object, owner: object, mint: object) -> None:
|
|
280
|
+
"""Create an associated token account (idempotent). Uses the SPL Token program."""
|
|
281
|
+
del payer, ata, owner, mint
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def system_transfer(src: object, dest: object, lamports: int) -> None:
|
|
285
|
+
"""Move lamports with a System-program transfer CPI.
|
|
286
|
+
|
|
287
|
+
``src`` and ``dest`` are a ``Signer`` or a ``Mut`` account, and they must
|
|
288
|
+
differ. The generated client appends the System program account.
|
|
289
|
+
"""
|
|
290
|
+
del src, dest, lamports
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def emit(value: object) -> None:
|
|
294
|
+
"""Log one ``@event``. The compiler lowers the call; this stub does not.
|
|
295
|
+
|
|
296
|
+
Write ``emit(Name(field, ...))`` with one positional argument per field,
|
|
297
|
+
in declaration order. The on-chain log is the 8-byte event discriminator
|
|
298
|
+
(``sha256("event:Name")[..8]``) followed by the packed fields.
|
|
299
|
+
"""
|
|
300
|
+
del value
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
class InstructionError(ProgramError):
|
|
304
|
+
"""Recommended base for @error classes used via `require(cond, cls.name)`."""
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def require(condition: bool, err: object) -> None:
|
|
308
|
+
"""Fail the instruction immediately if the condition is false.
|
|
309
|
+
|
|
310
|
+
The compiler front-end reads calls to this helper statically; `err` must be
|
|
311
|
+
a member of an ``@error`` class (e.g. ``require(cond, MyErr.Bad)``). The
|
|
312
|
+
static check is what enforces that; the Python signature stays permissive
|
|
313
|
+
so the decorator surface type-checks with plain mypy/pyright.
|
|
314
|
+
"""
|
|
315
|
+
|
|
316
|
+
if condition is not True: # pragma: no cover
|
|
317
|
+
raise err # type: ignore[misc] # DSL stub: real semantics live in the compiled C.
|
pythsol/cli/__init__.py
ADDED
pythsol/cli/cache.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Content-addressed cache for the SBF ``.so``.
|
|
2
|
+
|
|
3
|
+
The key is the generated C, the compiler version, the clang version, the
|
|
4
|
+
runtime header, and a flavor tag. A hit copies the ``.so`` and skips clang.
|
|
5
|
+
The C text is part of the key so an emitter change is a miss even when the
|
|
6
|
+
package version stays ``0.1.0``.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import hashlib
|
|
12
|
+
import os
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from pythsol import __version__
|
|
16
|
+
|
|
17
|
+
__all__ = ["artifact_key", "cache_directory", "lookup_so", "store_so"]
|
|
18
|
+
|
|
19
|
+
#: Bump when compile or link flags change but the C text might not.
|
|
20
|
+
CACHE_FLAVOR = "sbf-o2-gc-strip-v1"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def cache_directory() -> Path | None:
|
|
24
|
+
"""Return the cache root, or ``None`` when caching is off.
|
|
25
|
+
|
|
26
|
+
``PYTHSOL_NO_CACHE=1`` disables it. ``PYTHSOL_CACHE`` (or ``PYSOL_CACHE``)
|
|
27
|
+
overrides the default directory.
|
|
28
|
+
"""
|
|
29
|
+
flag = os.environ.get("PYTHSOL_NO_CACHE", "").strip().lower()
|
|
30
|
+
if flag in {"1", "true", "yes"}:
|
|
31
|
+
return None
|
|
32
|
+
override = os.environ.get("PYTHSOL_CACHE") or os.environ.get("PYSOL_CACHE")
|
|
33
|
+
if override:
|
|
34
|
+
return Path(override).expanduser()
|
|
35
|
+
local = os.environ.get("LOCALAPPDATA")
|
|
36
|
+
if local:
|
|
37
|
+
return Path(local) / "pythsol" / "cache"
|
|
38
|
+
return Path.home() / ".cache" / "pythsol"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def artifact_key(
|
|
42
|
+
*,
|
|
43
|
+
c_text: str,
|
|
44
|
+
clang_version: str,
|
|
45
|
+
arch: str,
|
|
46
|
+
runtime_header: bytes,
|
|
47
|
+
) -> str:
|
|
48
|
+
"""sha256 of everything that changes the linked ``.so``."""
|
|
49
|
+
digest = hashlib.sha256()
|
|
50
|
+
digest.update(b"pythsol-cache\0")
|
|
51
|
+
digest.update(CACHE_FLAVOR.encode())
|
|
52
|
+
digest.update(b"\0")
|
|
53
|
+
digest.update(__version__.encode())
|
|
54
|
+
digest.update(b"\0")
|
|
55
|
+
digest.update(clang_version.encode())
|
|
56
|
+
digest.update(b"\0")
|
|
57
|
+
digest.update(arch.encode())
|
|
58
|
+
digest.update(b"\0")
|
|
59
|
+
digest.update(hashlib.sha256(runtime_header).digest())
|
|
60
|
+
digest.update(c_text.encode())
|
|
61
|
+
return digest.hexdigest()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def lookup_so(root: Path, key: str) -> Path | None:
|
|
65
|
+
"""Return a cached ``.so`` for ``key``, or ``None``."""
|
|
66
|
+
path = root / key / "program.so"
|
|
67
|
+
if path.is_file() and path.stat().st_size > 0:
|
|
68
|
+
return path
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def store_so(root: Path, key: str, so_path: Path) -> None:
|
|
73
|
+
"""Copy ``so_path`` into the cache. A crash mid-write leaves the old file."""
|
|
74
|
+
dest_dir = root / key
|
|
75
|
+
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
76
|
+
temporary = dest_dir / "program.so.tmp"
|
|
77
|
+
temporary.write_bytes(so_path.read_bytes())
|
|
78
|
+
temporary.replace(dest_dir / "program.so")
|
pythsol/cli/cluster.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Cluster names and RPC URLs shared by deploy, faucet, and wallet."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
__all__ = ["DEVNET_URL", "is_mainnet", "resolve_url"]
|
|
8
|
+
|
|
9
|
+
DEVNET_URL = "https://api.devnet.solana.com"
|
|
10
|
+
MAINNET_URL = "https://api.mainnet-beta.solana.com"
|
|
11
|
+
LOCALHOST_URL = "http://127.0.0.1:8899"
|
|
12
|
+
TESTNET_URL = "https://api.testnet.solana.com"
|
|
13
|
+
|
|
14
|
+
_ALIASES = {
|
|
15
|
+
"devnet": DEVNET_URL,
|
|
16
|
+
"localhost": LOCALHOST_URL,
|
|
17
|
+
"localnet": LOCALHOST_URL,
|
|
18
|
+
"mainnet": MAINNET_URL,
|
|
19
|
+
"mainnet-beta": MAINNET_URL,
|
|
20
|
+
"testnet": TESTNET_URL,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
_MAINNET = {
|
|
24
|
+
MAINNET_URL.rstrip("/").lower(),
|
|
25
|
+
"https://api.mainnet.solana.com",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def resolve_url(value: str | None) -> str:
|
|
30
|
+
"""Turn a cluster name or URL into an RPC URL.
|
|
31
|
+
|
|
32
|
+
An empty value uses ``PYSOL_RPC_URL`` or ``PYTHSOL_RPC_URL``, then devnet.
|
|
33
|
+
"""
|
|
34
|
+
if value is None or value.strip() == "":
|
|
35
|
+
env = os.environ.get("PYSOL_RPC_URL") or os.environ.get("PYTHSOL_RPC_URL")
|
|
36
|
+
if env:
|
|
37
|
+
return resolve_url(env)
|
|
38
|
+
return DEVNET_URL
|
|
39
|
+
key = value.strip()
|
|
40
|
+
return _ALIASES.get(key.lower(), key)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def is_mainnet(value: str) -> bool:
|
|
44
|
+
"""True for mainnet aliases and the public mainnet RPC URL."""
|
|
45
|
+
return resolve_url(value).rstrip("/").lower() in _MAINNET
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""`pythsol build PROGRAM.py` — C → SBF `.so` + IDL + manifest."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import shutil
|
|
8
|
+
import sys
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
|
|
14
|
+
from pythsol import __version__
|
|
15
|
+
from pythsol.cli.cache import artifact_key, cache_directory, lookup_so, store_so
|
|
16
|
+
from pythsol.client.gen import render_client
|
|
17
|
+
from pythsol.compiler.diagnostics import Diagnostic, PysolError
|
|
18
|
+
from pythsol.compiler.idl import idl_json
|
|
19
|
+
from pythsol.compiler.modules import read_sibling_modules
|
|
20
|
+
from pythsol.compiler.pipeline import emit_program
|
|
21
|
+
from pythsol.runtime import HEADER_NAME, RUNTIME_INCLUDE_DIR
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class BuildFailed(Exception): # noqa: N818
|
|
25
|
+
"""The program did not pass the compiler. ``diagnostics`` is non-empty."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, diagnostics: list[Diagnostic]) -> None:
|
|
28
|
+
self.diagnostics = diagnostics
|
|
29
|
+
super().__init__(f"{len(diagnostics)} diagnostics")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class BuiltProgram:
|
|
34
|
+
"""Paths written by one successful build."""
|
|
35
|
+
|
|
36
|
+
name: str
|
|
37
|
+
workdir: Path
|
|
38
|
+
c_path: Path
|
|
39
|
+
so_path: Path
|
|
40
|
+
idl_path: Path
|
|
41
|
+
client_path: Path
|
|
42
|
+
manifest_path: Path
|
|
43
|
+
#: ``hit`` skipped clang, ``miss`` compiled, ``off`` cache disabled.
|
|
44
|
+
cache: str
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def build_program(
|
|
48
|
+
program: Path,
|
|
49
|
+
out: Path | None = None,
|
|
50
|
+
name: str | None = None,
|
|
51
|
+
) -> BuiltProgram:
|
|
52
|
+
"""Compile ``program`` and return the artifact paths.
|
|
53
|
+
|
|
54
|
+
Raises :class:`BuildFailed` for diagnostics, :class:`OSError` when a
|
|
55
|
+
source file cannot be read, and :class:`PysolError` when clang is missing
|
|
56
|
+
or the link fails.
|
|
57
|
+
"""
|
|
58
|
+
source_text = program.read_text(encoding="utf-8", errors="replace")
|
|
59
|
+
modules, module_files = read_sibling_modules(program)
|
|
60
|
+
generated = emit_program(
|
|
61
|
+
source_text,
|
|
62
|
+
program,
|
|
63
|
+
name=name,
|
|
64
|
+
modules=modules,
|
|
65
|
+
module_files=module_files,
|
|
66
|
+
)
|
|
67
|
+
if not generated.ok or generated.generated is None:
|
|
68
|
+
raise BuildFailed(list(generated.diagnostics))
|
|
69
|
+
|
|
70
|
+
workdir = (out or Path("target/pythsol") / generated.name).resolve()
|
|
71
|
+
workdir.mkdir(parents=True, exist_ok=True)
|
|
72
|
+
|
|
73
|
+
c_path = workdir / f"{generated.name}.c"
|
|
74
|
+
c_path.write_text(generated.c_text, encoding="utf-8", newline="\n")
|
|
75
|
+
|
|
76
|
+
from pythsol.compiler import toolchain
|
|
77
|
+
|
|
78
|
+
toolpaths = toolchain.locate_platform_tools(None)
|
|
79
|
+
if toolpaths is None:
|
|
80
|
+
raise PysolError(
|
|
81
|
+
code="PS4010",
|
|
82
|
+
message="no SBF platform-tools found; cannot finish the build",
|
|
83
|
+
fix="run `pythsol doctor`; set PYTHSOL_SBF_SDK or install Agave "
|
|
84
|
+
"platform-tools (CI Linux runners are the supported path)",
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
header_path = RUNTIME_INCLUDE_DIR / HEADER_NAME
|
|
88
|
+
header = header_path.read_bytes()
|
|
89
|
+
key = artifact_key(
|
|
90
|
+
c_text=generated.c_text,
|
|
91
|
+
clang_version=toolpaths.version or "unknown",
|
|
92
|
+
arch=toolpaths.arch,
|
|
93
|
+
runtime_header=header,
|
|
94
|
+
)
|
|
95
|
+
root = cache_directory()
|
|
96
|
+
cached = lookup_so(root, key) if root is not None else None
|
|
97
|
+
so_path = workdir / f"{generated.name}.so"
|
|
98
|
+
if cached is not None:
|
|
99
|
+
shutil.copyfile(cached, so_path)
|
|
100
|
+
cache = "hit"
|
|
101
|
+
else:
|
|
102
|
+
so_path = toolchain.compile_to_sbf(
|
|
103
|
+
toolpaths,
|
|
104
|
+
c_path,
|
|
105
|
+
workdir,
|
|
106
|
+
include_dirs=(RUNTIME_INCLUDE_DIR,),
|
|
107
|
+
)
|
|
108
|
+
if root is not None:
|
|
109
|
+
store_so(root, key, so_path)
|
|
110
|
+
cache = "miss" if root is not None else "off"
|
|
111
|
+
|
|
112
|
+
idl_path = workdir / f"{generated.name}.json"
|
|
113
|
+
idl_path.write_text(idl_json(generated.generated), encoding="utf-8", newline="\n")
|
|
114
|
+
|
|
115
|
+
client_path = workdir / f"{generated.name}_client.py"
|
|
116
|
+
client_path.write_text(
|
|
117
|
+
render_client(generated.generated),
|
|
118
|
+
encoding="utf-8",
|
|
119
|
+
newline="\n",
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
manifest = {
|
|
123
|
+
"generator": f"pythsol {__version__}",
|
|
124
|
+
"program": generated.name,
|
|
125
|
+
"source": generated.source_file,
|
|
126
|
+
"entrypoint": "entrypoint",
|
|
127
|
+
"cache": cache,
|
|
128
|
+
"files": {
|
|
129
|
+
"c": c_path.name,
|
|
130
|
+
"idl": idl_path.name,
|
|
131
|
+
"so": so_path.name,
|
|
132
|
+
"client": client_path.name,
|
|
133
|
+
},
|
|
134
|
+
"sha256": {
|
|
135
|
+
"c": _sha256(c_path),
|
|
136
|
+
"idl": _sha256(idl_path),
|
|
137
|
+
"so": _sha256(so_path),
|
|
138
|
+
"client": _sha256(client_path),
|
|
139
|
+
},
|
|
140
|
+
"toolchain": {
|
|
141
|
+
"arch": toolpaths.arch,
|
|
142
|
+
"clang": toolpaths.version or "unknown",
|
|
143
|
+
},
|
|
144
|
+
}
|
|
145
|
+
manifest_path = workdir / "manifest.json"
|
|
146
|
+
manifest_path.write_text(
|
|
147
|
+
json.dumps(manifest, indent=2, sort_keys=True) + "\n",
|
|
148
|
+
encoding="utf-8",
|
|
149
|
+
newline="\n",
|
|
150
|
+
)
|
|
151
|
+
return BuiltProgram(
|
|
152
|
+
name=generated.name,
|
|
153
|
+
workdir=workdir,
|
|
154
|
+
c_path=c_path,
|
|
155
|
+
so_path=so_path,
|
|
156
|
+
idl_path=idl_path,
|
|
157
|
+
client_path=client_path,
|
|
158
|
+
manifest_path=manifest_path,
|
|
159
|
+
cache=cache,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@click.command(name="build")
|
|
164
|
+
@click.argument(
|
|
165
|
+
"program",
|
|
166
|
+
metavar="PROGRAM.py",
|
|
167
|
+
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
|
168
|
+
)
|
|
169
|
+
@click.option(
|
|
170
|
+
"--out",
|
|
171
|
+
type=click.Path(file_okay=False, path_type=Path),
|
|
172
|
+
default=None,
|
|
173
|
+
help="Output directory (default: target/pythsol/<name>).",
|
|
174
|
+
)
|
|
175
|
+
@click.option(
|
|
176
|
+
"--name",
|
|
177
|
+
default=None,
|
|
178
|
+
help="Program name (default: the file stem).",
|
|
179
|
+
)
|
|
180
|
+
def build(program: Path, out: Path | None, name: str | None) -> None:
|
|
181
|
+
"""Compile a PythSol program to a native SBF `.so`, IDL and manifest.
|
|
182
|
+
|
|
183
|
+
Produces ``<name>.c`` (generated C), ``<name>.so`` (the on-chain ELF),
|
|
184
|
+
``<name>.json`` (IDL), ``<name>_client.py`` (solders builders) and
|
|
185
|
+
``manifest.json`` with sha256 hashes. Builds are byte-deterministic
|
|
186
|
+
for identical input. A second build of the same C skips clang when the
|
|
187
|
+
cache is on.
|
|
188
|
+
"""
|
|
189
|
+
try:
|
|
190
|
+
built = build_program(program, out=out, name=name)
|
|
191
|
+
except OSError as os_error:
|
|
192
|
+
click.echo(f"error: cannot read {program}: {os_error}", err=True)
|
|
193
|
+
sys.exit(2)
|
|
194
|
+
except BuildFailed as failed:
|
|
195
|
+
for diagnostic in failed.diagnostics:
|
|
196
|
+
click.echo(diagnostic.render(), err=True)
|
|
197
|
+
sys.exit(1)
|
|
198
|
+
except PysolError as error:
|
|
199
|
+
click.echo(error.render(), err=True)
|
|
200
|
+
sys.exit(1)
|
|
201
|
+
|
|
202
|
+
click.echo(f"build: {program} -> {built.so_path}")
|
|
203
|
+
if built.cache == "hit":
|
|
204
|
+
click.echo("build: cache hit (skipped clang)")
|
|
205
|
+
elif built.cache == "miss":
|
|
206
|
+
click.echo("build: cache miss")
|
|
207
|
+
click.echo(f"build: idl -> {built.idl_path}")
|
|
208
|
+
click.echo(f"build: client -> {built.client_path}")
|
|
209
|
+
click.echo(f"build: manifest -> {built.manifest_path}")
|
|
210
|
+
sys.exit(0)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _sha256(path: Path) -> str:
|
|
214
|
+
return hashlib.sha256(path.read_bytes()).hexdigest()
|