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,128 @@
|
|
|
1
|
+
"""`pythsol wallet` — new keypair, address, and balance.
|
|
2
|
+
|
|
3
|
+
The keypair file is the Solana JSON array (64 bytes). It is written mode
|
|
4
|
+
``0600``. The secret is not printed unless ``--show-secret`` is set.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import click
|
|
15
|
+
|
|
16
|
+
from pythsol.cli.cluster import resolve_url
|
|
17
|
+
from pythsol.cli.rpc import RpcError, post_json
|
|
18
|
+
from pythsol.compiler.toolchain import default_solana_keypair
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def restrict_private_file(path: Path) -> None:
|
|
22
|
+
"""Owner read/write only. On Windows, chmod is the best the CRT offers."""
|
|
23
|
+
os.chmod(path, 0o600)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _keypair_type() -> Any:
|
|
27
|
+
try:
|
|
28
|
+
from solders.keypair import Keypair
|
|
29
|
+
except ImportError:
|
|
30
|
+
click.echo("error: solders is required. pip install 'pythsol[test]'", err=True)
|
|
31
|
+
sys.exit(1)
|
|
32
|
+
return Keypair
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _load(path: Path) -> Any:
|
|
36
|
+
return _keypair_type().from_json(path.read_text(encoding="utf-8"))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _lamports(body: dict[str, object]) -> int:
|
|
40
|
+
result = body.get("result")
|
|
41
|
+
if isinstance(result, dict) and "value" in result:
|
|
42
|
+
return int(result["value"])
|
|
43
|
+
if isinstance(result, int):
|
|
44
|
+
return result
|
|
45
|
+
raise RpcError("balance response had no value")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@click.group(name="wallet")
|
|
49
|
+
def wallet() -> None:
|
|
50
|
+
"""Create a keypair, print its address, or read a balance."""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@wallet.command("new")
|
|
54
|
+
@click.option(
|
|
55
|
+
"--outfile",
|
|
56
|
+
required=True,
|
|
57
|
+
type=click.Path(dir_okay=False, path_type=Path),
|
|
58
|
+
help="Where to write the keypair JSON.",
|
|
59
|
+
)
|
|
60
|
+
@click.option("--force", is_flag=True, help="Overwrite an existing file.")
|
|
61
|
+
@click.option("--show-secret", is_flag=True, help="Print the keypair JSON. Off by default.")
|
|
62
|
+
def wallet_new(outfile: Path, force: bool, show_secret: bool) -> None:
|
|
63
|
+
"""Write a new keypair file."""
|
|
64
|
+
if outfile.exists() and not force:
|
|
65
|
+
click.echo(f"error: {outfile} already exists (pass --force to overwrite)", err=True)
|
|
66
|
+
sys.exit(1)
|
|
67
|
+
outfile.parent.mkdir(parents=True, exist_ok=True)
|
|
68
|
+
keypair = _keypair_type()()
|
|
69
|
+
text = keypair.to_json()
|
|
70
|
+
if not text.endswith("\n"):
|
|
71
|
+
text += "\n"
|
|
72
|
+
outfile.write_text(text, encoding="utf-8", newline="\n")
|
|
73
|
+
restrict_private_file(outfile)
|
|
74
|
+
click.echo(f"wrote {outfile}")
|
|
75
|
+
click.echo(str(keypair.pubkey()))
|
|
76
|
+
if show_secret:
|
|
77
|
+
click.echo(text, nl=False)
|
|
78
|
+
sys.exit(0)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@wallet.command("address")
|
|
82
|
+
@click.option(
|
|
83
|
+
"--keypair",
|
|
84
|
+
"keypair_path",
|
|
85
|
+
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
|
86
|
+
default=None,
|
|
87
|
+
)
|
|
88
|
+
@click.option("--show-secret", is_flag=True, help="Print the keypair JSON. Off by default.")
|
|
89
|
+
def wallet_address(keypair_path: Path | None, show_secret: bool) -> None:
|
|
90
|
+
"""Print the public key. The secret stays in the file."""
|
|
91
|
+
path = keypair_path or default_solana_keypair()
|
|
92
|
+
if not path.is_file():
|
|
93
|
+
click.echo(f"error: keypair not found at {path}", err=True)
|
|
94
|
+
sys.exit(1)
|
|
95
|
+
keypair = _load(path)
|
|
96
|
+
click.echo(str(keypair.pubkey()))
|
|
97
|
+
if show_secret:
|
|
98
|
+
click.echo(keypair.to_json())
|
|
99
|
+
sys.exit(0)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@wallet.command("balance")
|
|
103
|
+
@click.option("--url", default=None, help="Cluster name or RPC URL. Default: devnet.")
|
|
104
|
+
@click.option("--pubkey", default=None, help="Base58 address. Default: the local keypair.")
|
|
105
|
+
@click.option(
|
|
106
|
+
"--keypair",
|
|
107
|
+
"keypair_path",
|
|
108
|
+
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
|
109
|
+
default=None,
|
|
110
|
+
)
|
|
111
|
+
def wallet_balance(url: str | None, pubkey: str | None, keypair_path: Path | None) -> None:
|
|
112
|
+
"""Print the SOL balance of a public key."""
|
|
113
|
+
if pubkey is None:
|
|
114
|
+
path = keypair_path or default_solana_keypair()
|
|
115
|
+
if not path.is_file():
|
|
116
|
+
click.echo(f"error: keypair not found at {path}", err=True)
|
|
117
|
+
sys.exit(1)
|
|
118
|
+
pubkey = str(_load(path).pubkey())
|
|
119
|
+
cluster = resolve_url(url)
|
|
120
|
+
try:
|
|
121
|
+
body = post_json(cluster, "getBalance", [pubkey])
|
|
122
|
+
lamports = _lamports(body)
|
|
123
|
+
except RpcError as exc:
|
|
124
|
+
click.echo(f"error: {exc}", err=True)
|
|
125
|
+
sys.exit(1)
|
|
126
|
+
sol = lamports / 1_000_000_000
|
|
127
|
+
click.echo(f"{sol:.4f} SOL ({lamports} lamports)")
|
|
128
|
+
sys.exit(0)
|
pythsol/cli/config.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Read ``pysol.toml`` project files."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import tomllib
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
__all__ = ["ConfigError", "ProgramEntry", "ProjectConfig", "find_config", "load_config"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ConfigError(Exception):
|
|
14
|
+
"""``pysol.toml`` is missing a required field or has the wrong shape."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class ProgramEntry:
|
|
19
|
+
"""One ``[[programs]]`` row."""
|
|
20
|
+
|
|
21
|
+
name: str
|
|
22
|
+
entry: Path
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class ProjectConfig:
|
|
27
|
+
"""The fields `pythsol test` and `pythsol build` read from a project."""
|
|
28
|
+
|
|
29
|
+
path: Path
|
|
30
|
+
name: str
|
|
31
|
+
programs: tuple[ProgramEntry, ...]
|
|
32
|
+
out_dir: Path
|
|
33
|
+
cluster: str
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def find_config(start: Path) -> Path | None:
|
|
37
|
+
"""Walk from ``start`` toward the filesystem root looking for ``pysol.toml``."""
|
|
38
|
+
current = start.resolve()
|
|
39
|
+
if current.is_file():
|
|
40
|
+
current = current.parent
|
|
41
|
+
for directory in [current, *current.parents]:
|
|
42
|
+
candidate = directory / "pysol.toml"
|
|
43
|
+
if candidate.is_file():
|
|
44
|
+
return candidate
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def load_config(path: Path) -> ProjectConfig:
|
|
49
|
+
"""Parse ``path``. Raises :class:`ConfigError` on a bad shape."""
|
|
50
|
+
raw: dict[str, Any] = tomllib.loads(path.read_text(encoding="utf-8"))
|
|
51
|
+
project = raw.get("project")
|
|
52
|
+
build = raw.get("build")
|
|
53
|
+
cluster = raw.get("cluster")
|
|
54
|
+
programs_raw = raw.get("programs")
|
|
55
|
+
if not isinstance(programs_raw, list) or not programs_raw:
|
|
56
|
+
raise ConfigError("pysol.toml needs at least one [[programs]] entry")
|
|
57
|
+
root = path.parent
|
|
58
|
+
programs: list[ProgramEntry] = []
|
|
59
|
+
for item in programs_raw:
|
|
60
|
+
if not isinstance(item, dict):
|
|
61
|
+
raise ConfigError("[[programs]] entries must be tables")
|
|
62
|
+
name = item.get("name")
|
|
63
|
+
entry = item.get("entry")
|
|
64
|
+
if not isinstance(name, str) or not name:
|
|
65
|
+
raise ConfigError("a program needs a name")
|
|
66
|
+
if not isinstance(entry, str) or not entry:
|
|
67
|
+
raise ConfigError(f"program `{name}` needs an entry path")
|
|
68
|
+
programs.append(ProgramEntry(name, (root / entry).resolve()))
|
|
69
|
+
out_raw = "target/pythsol"
|
|
70
|
+
if isinstance(build, dict) and isinstance(build.get("out_dir"), str):
|
|
71
|
+
out_raw = build["out_dir"]
|
|
72
|
+
default_cluster = "devnet"
|
|
73
|
+
if isinstance(cluster, dict) and isinstance(cluster.get("default"), str):
|
|
74
|
+
default_cluster = cluster["default"]
|
|
75
|
+
project_name = root.name
|
|
76
|
+
if isinstance(project, dict) and isinstance(project.get("name"), str):
|
|
77
|
+
project_name = project["name"]
|
|
78
|
+
return ProjectConfig(
|
|
79
|
+
path=path,
|
|
80
|
+
name=project_name,
|
|
81
|
+
programs=tuple(programs),
|
|
82
|
+
out_dir=(root / out_raw).resolve(),
|
|
83
|
+
cluster=default_cluster,
|
|
84
|
+
)
|
pythsol/cli/main.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""CLI entry point (`pythsol`, aliased as the draft `pysol`)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
from pythsol import __version__
|
|
11
|
+
from pythsol.cli.commands.build import build
|
|
12
|
+
from pythsol.cli.commands.check import check
|
|
13
|
+
from pythsol.cli.commands.client_cmd import client_cmd, cpi_cmd
|
|
14
|
+
from pythsol.cli.commands.deploy import deploy
|
|
15
|
+
from pythsol.cli.commands.docs_cmd import docs
|
|
16
|
+
from pythsol.cli.commands.doctor import doctor
|
|
17
|
+
from pythsol.cli.commands.emit_c import emit_c
|
|
18
|
+
from pythsol.cli.commands.faucet import faucet
|
|
19
|
+
from pythsol.cli.commands.idl_cmd import idl
|
|
20
|
+
from pythsol.cli.commands.idl_publish import idl_publish
|
|
21
|
+
from pythsol.cli.commands.init import init
|
|
22
|
+
from pythsol.cli.commands.port_cmd import port_cmd
|
|
23
|
+
from pythsol.cli.commands.profile import profile
|
|
24
|
+
from pythsol.cli.commands.serve import serve
|
|
25
|
+
from pythsol.cli.commands.test_cmd import test_cmd
|
|
26
|
+
from pythsol.cli.commands.verify import verify
|
|
27
|
+
from pythsol.cli.commands.wallet import wallet
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@click.group(context_settings={"help_option_names": ["-h", "--help"]})
|
|
31
|
+
@click.version_option(__version__, "--version", message="%(prog)s %(version)s")
|
|
32
|
+
@click.option("--quiet", is_flag=True, help="Silence non-essential output.")
|
|
33
|
+
@click.option("--verbose", is_flag=True, help="Show external commands that are run.")
|
|
34
|
+
def cli(quiet: bool, verbose: bool) -> None:
|
|
35
|
+
"""PythSol — Solana programs in typed Python."""
|
|
36
|
+
# Per-command implementations use these flags; Phase 1 only needs doctor.
|
|
37
|
+
_ = quiet, verbose
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def main(prog_name: str | None = None) -> None:
|
|
41
|
+
"""Module entry point: "pythsol" or "pysol" depending on packaging aliases."""
|
|
42
|
+
prog = prog_name or os.path.basename(sys.argv[0]) or "pythsol"
|
|
43
|
+
cli(prog_name=prog) # `Group.__call__` forwards to `Group.main(prog_name=prog)`.
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# Re-attach commands after the group definition so auto-help works uniformly.
|
|
47
|
+
cli.add_command(doctor)
|
|
48
|
+
cli.add_command(check)
|
|
49
|
+
cli.add_command(emit_c)
|
|
50
|
+
cli.add_command(build)
|
|
51
|
+
cli.add_command(test_cmd)
|
|
52
|
+
cli.add_command(init)
|
|
53
|
+
cli.add_command(idl)
|
|
54
|
+
cli.add_command(idl_publish)
|
|
55
|
+
cli.add_command(client_cmd)
|
|
56
|
+
cli.add_command(cpi_cmd)
|
|
57
|
+
cli.add_command(deploy)
|
|
58
|
+
cli.add_command(faucet)
|
|
59
|
+
cli.add_command(wallet)
|
|
60
|
+
cli.add_command(port_cmd)
|
|
61
|
+
cli.add_command(verify)
|
|
62
|
+
cli.add_command(profile)
|
|
63
|
+
cli.add_command(docs)
|
|
64
|
+
cli.add_command(serve)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _entrypoint() -> None: # pragma: no cover — packaging script
|
|
68
|
+
main()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
if __name__ == "__main__": # pragma: no cover
|
|
72
|
+
_entrypoint()
|
pythsol/cli/rpc.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""JSON-RPC helper for faucet and wallet balance.
|
|
2
|
+
|
|
3
|
+
The compiler never calls this. Commands that talk to a cluster the user named
|
|
4
|
+
do. The body is a JSON-RPC request and the response is parsed as JSON.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from typing import Any
|
|
11
|
+
from urllib.error import HTTPError, URLError
|
|
12
|
+
from urllib.request import Request, urlopen
|
|
13
|
+
|
|
14
|
+
__all__ = ["RpcError", "post_json"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class RpcError(Exception):
|
|
18
|
+
"""The endpoint refused the call or returned an error object."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def post_json(
|
|
22
|
+
url: str,
|
|
23
|
+
method: str,
|
|
24
|
+
params: list[object],
|
|
25
|
+
*,
|
|
26
|
+
timeout: float = 20.0,
|
|
27
|
+
) -> dict[str, Any]:
|
|
28
|
+
"""POST one JSON-RPC request and return the decoded object."""
|
|
29
|
+
payload = json.dumps({"jsonrpc": "2.0", "id": 1, "method": method, "params": params}).encode(
|
|
30
|
+
"utf-8"
|
|
31
|
+
)
|
|
32
|
+
request = Request(
|
|
33
|
+
url,
|
|
34
|
+
data=payload,
|
|
35
|
+
headers={"content-type": "application/json"},
|
|
36
|
+
method="POST",
|
|
37
|
+
)
|
|
38
|
+
try:
|
|
39
|
+
with urlopen(request, timeout=timeout) as response:
|
|
40
|
+
raw = response.read()
|
|
41
|
+
except HTTPError as exc:
|
|
42
|
+
detail = exc.read().decode("utf-8", errors="replace")
|
|
43
|
+
raise RpcError(detail or f"HTTP {exc.code}") from exc
|
|
44
|
+
except URLError as exc:
|
|
45
|
+
raise RpcError(str(exc.reason)) from exc
|
|
46
|
+
try:
|
|
47
|
+
body = json.loads(raw.decode("utf-8"))
|
|
48
|
+
except json.JSONDecodeError as exc:
|
|
49
|
+
raise RpcError("RPC response was not JSON") from exc
|
|
50
|
+
if not isinstance(body, dict):
|
|
51
|
+
raise RpcError("RPC response was not an object")
|
|
52
|
+
if "error" in body and body["error"] is not None:
|
|
53
|
+
error = body["error"]
|
|
54
|
+
message = error.get("message") if isinstance(error, dict) else str(error)
|
|
55
|
+
raise RpcError(str(message))
|
|
56
|
+
return body
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Embedded `pythsol init` program sources. Read as text; do not import them."""
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""A minimal PythSol program: an owned counter.
|
|
3
|
+
|
|
4
|
+
`pythsol check programs/counter.py` must pass `mypy --strict` *and* the
|
|
5
|
+
PythSol front-end. `pythsol build` (Phase 3) compiles it to native SBF.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pythsol import (
|
|
9
|
+
Init,
|
|
10
|
+
Mut,
|
|
11
|
+
ProgramError,
|
|
12
|
+
Pubkey,
|
|
13
|
+
Signer,
|
|
14
|
+
account,
|
|
15
|
+
error,
|
|
16
|
+
instruction,
|
|
17
|
+
require,
|
|
18
|
+
u8,
|
|
19
|
+
u64,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@account
|
|
24
|
+
class Counter:
|
|
25
|
+
"""The on-chain counter account layout."""
|
|
26
|
+
|
|
27
|
+
authority: Pubkey
|
|
28
|
+
count: u64
|
|
29
|
+
bump: u8
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@error
|
|
33
|
+
class CounterError(ProgramError):
|
|
34
|
+
"""Errors returned by this program (u32 codes)."""
|
|
35
|
+
|
|
36
|
+
Unauthorized = 100
|
|
37
|
+
Overflow = 101
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@instruction
|
|
41
|
+
def initialize(authority: Signer, counter: Init[Counter], bump: u8) -> None:
|
|
42
|
+
"""Create and initialize the counter authority fields."""
|
|
43
|
+
counter.authority = authority.key
|
|
44
|
+
counter.count = 0
|
|
45
|
+
counter.bump = bump
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@instruction
|
|
49
|
+
def increment(authority: Signer, counter: Mut[Counter], by: u64) -> None:
|
|
50
|
+
"""Add `by` to the count; only the stored authority may call this."""
|
|
51
|
+
require(counter.authority == authority.key, CounterError.Unauthorized)
|
|
52
|
+
counter.count += by
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Escrow: one PDA deal funded in lamports.
|
|
2
|
+
|
|
3
|
+
The seed is the literal ``escrow``, so a program has one open deal. ``make``
|
|
4
|
+
creates it and transfers ``amount`` in. ``take`` pays the maker, then closes
|
|
5
|
+
the account to the taker. ``cancel`` closes it back to the maker. A
|
|
6
|
+
PDA-signed transfer is not available; closing the account is what returns
|
|
7
|
+
the escrowed lamports.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from pythsol import (
|
|
11
|
+
CloseTo,
|
|
12
|
+
Init,
|
|
13
|
+
Mut,
|
|
14
|
+
ProgramError,
|
|
15
|
+
Pubkey,
|
|
16
|
+
Seeds,
|
|
17
|
+
Signer,
|
|
18
|
+
account,
|
|
19
|
+
error,
|
|
20
|
+
instruction,
|
|
21
|
+
require,
|
|
22
|
+
system_transfer,
|
|
23
|
+
u8,
|
|
24
|
+
u64,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@account
|
|
29
|
+
class Escrow:
|
|
30
|
+
maker: Pubkey
|
|
31
|
+
taker: Pubkey
|
|
32
|
+
amount: u64
|
|
33
|
+
bump: u8
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@error
|
|
37
|
+
class EscrowError(ProgramError):
|
|
38
|
+
"""Errors returned by this program (6000 + index)."""
|
|
39
|
+
|
|
40
|
+
Unauthorized = 100
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@instruction
|
|
44
|
+
def make(
|
|
45
|
+
maker: Signer,
|
|
46
|
+
escrow: Seeds[Init[Escrow], "escrow"], # type: ignore[name-defined] # noqa: F821
|
|
47
|
+
taker: Pubkey,
|
|
48
|
+
bump: u8,
|
|
49
|
+
amount: u64,
|
|
50
|
+
) -> None:
|
|
51
|
+
escrow.maker = maker.key
|
|
52
|
+
escrow.taker = taker
|
|
53
|
+
escrow.amount = amount
|
|
54
|
+
escrow.bump = bump
|
|
55
|
+
system_transfer(maker, escrow, amount)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@instruction
|
|
59
|
+
def take(
|
|
60
|
+
taker: Signer,
|
|
61
|
+
maker: Signer,
|
|
62
|
+
escrow: CloseTo[Mut[Escrow], taker], # type: ignore[name-defined] # noqa: F821
|
|
63
|
+
price: u64,
|
|
64
|
+
) -> None:
|
|
65
|
+
require(escrow.taker == taker.key, EscrowError.Unauthorized)
|
|
66
|
+
require(escrow.maker == maker.key, EscrowError.Unauthorized)
|
|
67
|
+
system_transfer(taker, maker, price)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@instruction
|
|
71
|
+
def cancel(
|
|
72
|
+
maker: Signer,
|
|
73
|
+
escrow: CloseTo[Mut[Escrow], maker], # type: ignore[name-defined] # noqa: F821
|
|
74
|
+
) -> None:
|
|
75
|
+
require(escrow.maker == maker.key, EscrowError.Unauthorized)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Ledger program: fixed slots, deposits, and a checked running total."""
|
|
2
|
+
|
|
3
|
+
from pythsol import (
|
|
4
|
+
FixedArray,
|
|
5
|
+
Init,
|
|
6
|
+
Mut,
|
|
7
|
+
ProgramError,
|
|
8
|
+
Pubkey,
|
|
9
|
+
Signer,
|
|
10
|
+
account,
|
|
11
|
+
error,
|
|
12
|
+
instruction,
|
|
13
|
+
require,
|
|
14
|
+
u8,
|
|
15
|
+
u64,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@account
|
|
20
|
+
class Ledger:
|
|
21
|
+
authority: Pubkey
|
|
22
|
+
total: u64
|
|
23
|
+
slots: FixedArray[u64, 4] # type: ignore[valid-type]
|
|
24
|
+
bump: u8
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@error
|
|
28
|
+
class LedgerError(ProgramError):
|
|
29
|
+
"""Errors returned by this program (6000 + index)."""
|
|
30
|
+
|
|
31
|
+
Unauthorized = 100
|
|
32
|
+
BadSlot = 101
|
|
33
|
+
Insufficient = 102
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@instruction
|
|
37
|
+
def initialize(authority: Signer, ledger: Init[Ledger], bump: u8) -> None:
|
|
38
|
+
ledger.authority = authority.key
|
|
39
|
+
ledger.total = 0
|
|
40
|
+
ledger.bump = bump
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@instruction
|
|
44
|
+
def deposit(authority: Signer, ledger: Mut[Ledger], slot: u8, amount: u64) -> None:
|
|
45
|
+
require(ledger.authority == authority.key, LedgerError.Unauthorized)
|
|
46
|
+
require(slot < 4, LedgerError.BadSlot)
|
|
47
|
+
ledger.slots[slot] = ledger.slots[slot] + amount
|
|
48
|
+
ledger.total = ledger.total + amount
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@instruction
|
|
52
|
+
def apply(authority: Signer, ledger: Mut[Ledger], amount: u64, mode: u8) -> None:
|
|
53
|
+
require(ledger.authority == authority.key, LedgerError.Unauthorized)
|
|
54
|
+
if mode == 0:
|
|
55
|
+
ledger.total = ledger.total + amount
|
|
56
|
+
else:
|
|
57
|
+
require(ledger.total >= amount, LedgerError.Insufficient)
|
|
58
|
+
ledger.total = ledger.total - amount
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@instruction
|
|
62
|
+
def fill(authority: Signer, ledger: Mut[Ledger], value: u64) -> None:
|
|
63
|
+
require(ledger.authority == authority.key, LedgerError.Unauthorized)
|
|
64
|
+
total: u64 = 0
|
|
65
|
+
for i in range(4):
|
|
66
|
+
ledger.slots[i] = value
|
|
67
|
+
total = total + value
|
|
68
|
+
ledger.total = total
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""A 2-of-3 treasury. Owners approve a payout; the PDA signs the transfer."""
|
|
2
|
+
|
|
3
|
+
from pythsol import (
|
|
4
|
+
FixedArray,
|
|
5
|
+
Init,
|
|
6
|
+
Mut,
|
|
7
|
+
ProgramError,
|
|
8
|
+
Pubkey,
|
|
9
|
+
Seeds,
|
|
10
|
+
Signer,
|
|
11
|
+
account,
|
|
12
|
+
error,
|
|
13
|
+
instruction,
|
|
14
|
+
require,
|
|
15
|
+
system_transfer,
|
|
16
|
+
u8,
|
|
17
|
+
u64,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@account
|
|
22
|
+
class Treasury:
|
|
23
|
+
creator: Pubkey
|
|
24
|
+
owners: FixedArray[Pubkey, 3]
|
|
25
|
+
approvals: u8
|
|
26
|
+
threshold: u8
|
|
27
|
+
paid: u64
|
|
28
|
+
bump: u8
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@error
|
|
32
|
+
class TreasuryError(ProgramError):
|
|
33
|
+
"""Errors returned by this program (6000 + index)."""
|
|
34
|
+
|
|
35
|
+
Unauthorized = 100
|
|
36
|
+
BadSlot = 101
|
|
37
|
+
AlreadyApproved = 102
|
|
38
|
+
NotApproved = 103
|
|
39
|
+
Threshold = 104
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@instruction
|
|
43
|
+
def initialize(
|
|
44
|
+
authority: Signer,
|
|
45
|
+
treasury: Seeds[Init[Treasury], "treasury"], # type: ignore[name-defined] # noqa: F821
|
|
46
|
+
first: Pubkey,
|
|
47
|
+
second: Pubkey,
|
|
48
|
+
third: Pubkey,
|
|
49
|
+
threshold: u8,
|
|
50
|
+
bump: u8,
|
|
51
|
+
) -> None:
|
|
52
|
+
require(threshold >= 1, TreasuryError.Threshold)
|
|
53
|
+
require(threshold <= 3, TreasuryError.Threshold)
|
|
54
|
+
treasury.creator = authority.key
|
|
55
|
+
treasury.owners[0] = first
|
|
56
|
+
treasury.owners[1] = second
|
|
57
|
+
treasury.owners[2] = third
|
|
58
|
+
treasury.approvals = 0
|
|
59
|
+
treasury.threshold = threshold
|
|
60
|
+
treasury.paid = 0
|
|
61
|
+
treasury.bump = bump
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@instruction
|
|
65
|
+
def fund(donor: Signer, treasury: Mut[Treasury], amount: u64) -> None:
|
|
66
|
+
system_transfer(donor, treasury, amount)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@instruction
|
|
70
|
+
def approve(owner: Signer, treasury: Mut[Treasury], slot: u8) -> None:
|
|
71
|
+
require(slot < 3, TreasuryError.BadSlot)
|
|
72
|
+
require(treasury.owners[slot] == owner.key, TreasuryError.Unauthorized)
|
|
73
|
+
mask: u8 = 1 << slot
|
|
74
|
+
require((treasury.approvals & mask) == 0, TreasuryError.AlreadyApproved)
|
|
75
|
+
treasury.approvals = treasury.approvals | mask
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@instruction
|
|
79
|
+
def release(owner: Signer, treasury: Mut[Treasury], dest: Signer, slot: u8, amount: u64) -> None:
|
|
80
|
+
require(slot < 3, TreasuryError.BadSlot)
|
|
81
|
+
require(treasury.owners[slot] == owner.key, TreasuryError.Unauthorized)
|
|
82
|
+
mask: u8 = 1 << slot
|
|
83
|
+
require((treasury.approvals & mask) != 0, TreasuryError.NotApproved)
|
|
84
|
+
count: u8 = 0
|
|
85
|
+
for i in range(3):
|
|
86
|
+
if ((treasury.approvals >> i) & 1) == 1:
|
|
87
|
+
count = count + 1
|
|
88
|
+
require(count >= treasury.threshold, TreasuryError.Threshold)
|
|
89
|
+
treasury.approvals = 0
|
|
90
|
+
system_transfer(treasury, dest, amount)
|
|
91
|
+
treasury.paid = treasury.paid + amount
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""A four-voter poll. Each allowlisted key votes once; the tally lives on chain."""
|
|
2
|
+
|
|
3
|
+
from pythsol import (
|
|
4
|
+
FixedArray,
|
|
5
|
+
Init,
|
|
6
|
+
Mut,
|
|
7
|
+
ProgramError,
|
|
8
|
+
Pubkey,
|
|
9
|
+
Seeds,
|
|
10
|
+
Signer,
|
|
11
|
+
account,
|
|
12
|
+
error,
|
|
13
|
+
instruction,
|
|
14
|
+
require,
|
|
15
|
+
u8,
|
|
16
|
+
u32,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@account
|
|
21
|
+
class Poll:
|
|
22
|
+
authority: Pubkey
|
|
23
|
+
voters: FixedArray[Pubkey, 4]
|
|
24
|
+
voted: u8
|
|
25
|
+
yes: u32
|
|
26
|
+
no: u32
|
|
27
|
+
bump: u8
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@error
|
|
31
|
+
class PollError(ProgramError):
|
|
32
|
+
"""Errors returned by this program (6000 + index)."""
|
|
33
|
+
|
|
34
|
+
BadSlot = 100
|
|
35
|
+
Unauthorized = 101
|
|
36
|
+
AlreadyVoted = 102
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@instruction
|
|
40
|
+
def initialize(
|
|
41
|
+
authority: Signer,
|
|
42
|
+
poll: Seeds[Init[Poll], "poll"], # type: ignore[name-defined] # noqa: F821
|
|
43
|
+
first: Pubkey,
|
|
44
|
+
second: Pubkey,
|
|
45
|
+
third: Pubkey,
|
|
46
|
+
fourth: Pubkey,
|
|
47
|
+
bump: u8,
|
|
48
|
+
) -> None:
|
|
49
|
+
poll.authority = authority.key
|
|
50
|
+
poll.voters[0] = first
|
|
51
|
+
poll.voters[1] = second
|
|
52
|
+
poll.voters[2] = third
|
|
53
|
+
poll.voters[3] = fourth
|
|
54
|
+
poll.voted = 0
|
|
55
|
+
poll.yes = 0
|
|
56
|
+
poll.no = 0
|
|
57
|
+
poll.bump = bump
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@instruction
|
|
61
|
+
def vote(voter: Signer, poll: Mut[Poll], slot: u8, choice: bool) -> None:
|
|
62
|
+
require(slot < 4, PollError.BadSlot)
|
|
63
|
+
require(poll.voters[slot] == voter.key, PollError.Unauthorized)
|
|
64
|
+
mask: u8 = 1 << slot
|
|
65
|
+
require((poll.voted & mask) == 0, PollError.AlreadyVoted)
|
|
66
|
+
poll.voted = poll.voted | mask
|
|
67
|
+
if choice:
|
|
68
|
+
poll.yes = poll.yes + 1
|
|
69
|
+
else:
|
|
70
|
+
poll.no = poll.no + 1
|