casthon-sdk 0.1.0a1__tar.gz
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.
- casthon_sdk-0.1.0a1/PKG-INFO +221 -0
- casthon_sdk-0.1.0a1/README.md +199 -0
- casthon_sdk-0.1.0a1/pyproject.toml +46 -0
- casthon_sdk-0.1.0a1/setup.cfg +4 -0
- casthon_sdk-0.1.0a1/src/casthon/__init__.py +116 -0
- casthon_sdk-0.1.0a1/src/casthon/_cl_types.py +104 -0
- casthon_sdk-0.1.0a1/src/casthon/_context.py +15 -0
- casthon_sdk-0.1.0a1/src/casthon/_contract_schema.py +153 -0
- casthon_sdk-0.1.0a1/src/casthon/_dispatch.py +26 -0
- casthon_sdk-0.1.0a1/src/casthon/_entry_point_signature.py +58 -0
- casthon_sdk-0.1.0a1/src/casthon/_event_schema.py +78 -0
- casthon_sdk-0.1.0a1/src/casthon/_ffi.py +12 -0
- casthon_sdk-0.1.0a1/src/casthon/_signatures.py +134 -0
- casthon_sdk-0.1.0a1/src/casthon/_wire_types.py +87 -0
- casthon_sdk-0.1.0a1/src/casthon/api_error.py +15 -0
- casthon_sdk-0.1.0a1/src/casthon/arguments.py +45 -0
- casthon_sdk-0.1.0a1/src/casthon/bytesrepr.py +56 -0
- casthon_sdk-0.1.0a1/src/casthon/caller.py +125 -0
- casthon_sdk-0.1.0a1/src/casthon/ces.py +48 -0
- casthon_sdk-0.1.0a1/src/casthon/codec.py +242 -0
- casthon_sdk-0.1.0a1/src/casthon/contract_api/__init__.py +1 -0
- casthon_sdk-0.1.0a1/src/casthon/contract_api/account.py +35 -0
- casthon_sdk-0.1.0a1/src/casthon/contract_api/cryptography.py +22 -0
- casthon_sdk-0.1.0a1/src/casthon/contract_api/runtime.py +324 -0
- casthon_sdk-0.1.0a1/src/casthon/contract_api/storage.py +195 -0
- casthon_sdk-0.1.0a1/src/casthon/contract_api/system.py +77 -0
- casthon_sdk-0.1.0a1/src/casthon/contract_call.py +42 -0
- casthon_sdk-0.1.0a1/src/casthon/contracts.py +255 -0
- casthon_sdk-0.1.0a1/src/casthon/crypto.py +70 -0
- casthon_sdk-0.1.0a1/src/casthon/entry_points.py +85 -0
- casthon_sdk-0.1.0a1/src/casthon/enums.py +96 -0
- casthon_sdk-0.1.0a1/src/casthon/errors.py +96 -0
- casthon_sdk-0.1.0a1/src/casthon/events.py +222 -0
- casthon_sdk-0.1.0a1/src/casthon/key.py +545 -0
- casthon_sdk-0.1.0a1/src/casthon/messages.py +24 -0
- casthon_sdk-0.1.0a1/src/casthon/py.typed +0 -0
- casthon_sdk-0.1.0a1/src/casthon/references.py +250 -0
- casthon_sdk-0.1.0a1/src/casthon/return_value.py +79 -0
- casthon_sdk-0.1.0a1/src/casthon/testing/__init__.py +64 -0
- casthon_sdk-0.1.0a1/src/casthon/testing/build.py +120 -0
- casthon_sdk-0.1.0a1/src/casthon/testing/contract.py +120 -0
- casthon_sdk-0.1.0a1/src/casthon/testing/devnet.py +579 -0
- casthon_sdk-0.1.0a1/src/casthon/testing/live.py +87 -0
- casthon_sdk-0.1.0a1/src/casthon/testing/mock.py +453 -0
- casthon_sdk-0.1.0a1/src/casthon/testing/prints.py +80 -0
- casthon_sdk-0.1.0a1/src/casthon/testing/values.py +116 -0
- casthon_sdk-0.1.0a1/src/casthon/types.py +54 -0
- casthon_sdk-0.1.0a1/src/casthon/values.py +110 -0
- casthon_sdk-0.1.0a1/src/casthon_sdk.egg-info/PKG-INFO +221 -0
- casthon_sdk-0.1.0a1/src/casthon_sdk.egg-info/SOURCES.txt +75 -0
- casthon_sdk-0.1.0a1/src/casthon_sdk.egg-info/dependency_links.txt +1 -0
- casthon_sdk-0.1.0a1/src/casthon_sdk.egg-info/requires.txt +8 -0
- casthon_sdk-0.1.0a1/src/casthon_sdk.egg-info/top_level.txt +1 -0
- casthon_sdk-0.1.0a1/tests/test_argument_loading.py +95 -0
- casthon_sdk-0.1.0a1/tests/test_build_cache.py +82 -0
- casthon_sdk-0.1.0a1/tests/test_build_progress.py +49 -0
- casthon_sdk-0.1.0a1/tests/test_cep18.py +422 -0
- casthon_sdk-0.1.0a1/tests/test_cep78.py +527 -0
- casthon_sdk-0.1.0a1/tests/test_class_contracts.py +511 -0
- casthon_sdk-0.1.0a1/tests/test_contract_api.py +373 -0
- casthon_sdk-0.1.0a1/tests/test_contract_calls.py +196 -0
- casthon_sdk-0.1.0a1/tests/test_contract_packages.py +115 -0
- casthon_sdk-0.1.0a1/tests/test_contract_testing.py +234 -0
- casthon_sdk-0.1.0a1/tests/test_devnet_testing.py +292 -0
- casthon_sdk-0.1.0a1/tests/test_entry_point_factory.py +205 -0
- casthon_sdk-0.1.0a1/tests/test_enum_types.py +147 -0
- casthon_sdk-0.1.0a1/tests/test_events.py +396 -0
- casthon_sdk-0.1.0a1/tests/test_extended_api.py +321 -0
- casthon_sdk-0.1.0a1/tests/test_ffi_boundary.py +140 -0
- casthon_sdk-0.1.0a1/tests/test_named_keys.py +178 -0
- casthon_sdk-0.1.0a1/tests/test_native_unwind.py +29 -0
- casthon_sdk-0.1.0a1/tests/test_optional_args.py +186 -0
- casthon_sdk-0.1.0a1/tests/test_print_capture.py +84 -0
- casthon_sdk-0.1.0a1/tests/test_references.py +312 -0
- casthon_sdk-0.1.0a1/tests/test_return_values.py +103 -0
- casthon_sdk-0.1.0a1/tests/test_sdk.py +304 -0
- casthon_sdk-0.1.0a1/tests/test_typed_exports.py +237 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: casthon-sdk
|
|
3
|
+
Version: 0.1.0a1
|
|
4
|
+
Summary: Python SDK for writing Casper smart contracts with Casthon
|
|
5
|
+
Project-URL: Repository, https://github.com/veles-labs/casthon
|
|
6
|
+
Project-URL: Issues, https://github.com/veles-labs/casthon/issues
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Classifier: Typing :: Typed
|
|
15
|
+
Requires-Python: >=3.11
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
Requires-Dist: typing_extensions>=4.12; python_version < "3.12"
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: mypy==1.19.1; extra == "dev"
|
|
20
|
+
Requires-Dist: wasmtime==49.0.0; extra == "dev"
|
|
21
|
+
Requires-Dist: pytest==9.1.1; extra == "dev"
|
|
22
|
+
|
|
23
|
+
<p align="center">
|
|
24
|
+
<img src="docs/assets/casthon-logo.png" width="176" alt="A friendly ghost with a curled tail, inspired by the snake emoji">
|
|
25
|
+
</p>
|
|
26
|
+
|
|
27
|
+
# Casthon
|
|
28
|
+
|
|
29
|
+
**Python smart contracts for Casper.**
|
|
30
|
+
|
|
31
|
+
Write a contract as a Python class, give its entrypoints type annotations, and
|
|
32
|
+
build it into Casper-compatible WebAssembly. Casthon freezes your code with
|
|
33
|
+
MicroPython; the `casthon` SDK handles arguments, storage, return values and host calls.
|
|
34
|
+
|
|
35
|
+
The same contract source runs in ordinary Python tests. You can compose classes,
|
|
36
|
+
catch exceptions, and use `finally` for cleanup. An uncaught contract error becomes
|
|
37
|
+
a Casper revert at the outer invocation boundary.
|
|
38
|
+
|
|
39
|
+
## Hello, Casper
|
|
40
|
+
|
|
41
|
+
Here is [hello.py](contracts/hello.py), including its installer:
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from casthon import ContractHash, ContractPackageHash, U32, URef, UserError, export
|
|
45
|
+
from casthon.contract_api import storage
|
|
46
|
+
from casthon.contract_api.runtime import named_keys
|
|
47
|
+
from casthon.contracts import Contract, entry_point, export_contract
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@export_contract
|
|
51
|
+
class Hello(Contract):
|
|
52
|
+
calls: URef[U32]
|
|
53
|
+
|
|
54
|
+
@entry_point
|
|
55
|
+
def greet(self, name: str) -> str:
|
|
56
|
+
try:
|
|
57
|
+
name = name.strip()
|
|
58
|
+
if not name:
|
|
59
|
+
raise ValueError("Empty name")
|
|
60
|
+
if len(name) > 64:
|
|
61
|
+
raise UserError(1)
|
|
62
|
+
return f"Hello, {name}!"
|
|
63
|
+
except ValueError:
|
|
64
|
+
return "Hello, stranger!"
|
|
65
|
+
finally:
|
|
66
|
+
self.calls.write(self.calls.read() + 1)
|
|
67
|
+
print("Greeting finished")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
installed = named_keys.reference("hello", ContractHash)
|
|
71
|
+
installed_package = named_keys.reference("hello_package", ContractPackageHash)
|
|
72
|
+
installed_access = named_keys.reference("hello_access", URef[None])
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@export
|
|
76
|
+
def call() -> None:
|
|
77
|
+
package, access = storage.create_contract_package_at_hash()
|
|
78
|
+
contract, _ = package.add_contract_version(Hello(calls=0))
|
|
79
|
+
installed.set(contract)
|
|
80
|
+
installed_package.set(package)
|
|
81
|
+
installed_access.set(access)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`greet` receives and returns a Python string. `calls` is persistent U32 state.
|
|
85
|
+
An empty name takes the `except` branch. A name longer than 64 characters raises
|
|
86
|
+
a user error and reverts. The `finally` block runs in either case; a revert rolls
|
|
87
|
+
back its storage write too.
|
|
88
|
+
|
|
89
|
+
`@entry_point` describes the class ABI. `@export_contract` selects its Wasm
|
|
90
|
+
exports. The top-level `call` creates the package and stores the deployed hashes
|
|
91
|
+
explicitly. [The Python API guide](docs/python-api.md) explains the boundaries.
|
|
92
|
+
|
|
93
|
+
## Try it
|
|
94
|
+
|
|
95
|
+
This developer preview builds from a source checkout. You need Python 3.11+,
|
|
96
|
+
`uv`, Rust/Cargo, Node, Make, a host C compiler, Emscripten and Binaryen.
|
|
97
|
+
The [CLI guide](docs/casthon.md#requirements) covers the build requirements.
|
|
98
|
+
|
|
99
|
+
```sh
|
|
100
|
+
make setup
|
|
101
|
+
make cli
|
|
102
|
+
./target/release/casthon doctor
|
|
103
|
+
./target/release/casthon test -q
|
|
104
|
+
./target/release/casthon build hello
|
|
105
|
+
./target/release/casthon inspect build/hello.wasm
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The build produces `build/hello.wasm`, its metered test counterpart, and a build
|
|
109
|
+
report. It compiles the contract; deployment is a separate transaction.
|
|
110
|
+
|
|
111
|
+
Debug builds keep `print()` and exception tracebacks. For a release build:
|
|
112
|
+
|
|
113
|
+
```sh
|
|
114
|
+
./target/release/casthon build hello --release
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
That writes `build/release/hello.wasm` and removes the `casper_print` import.
|
|
118
|
+
See [CLI usage](docs/casthon.md) for project configuration, templates, caching
|
|
119
|
+
and minification.
|
|
120
|
+
|
|
121
|
+
## A few more examples
|
|
122
|
+
|
|
123
|
+
- [Counter](contracts/counter.py) composes ownership and pause controls, with a
|
|
124
|
+
typed URef and a persistent dictionary.
|
|
125
|
+
- [Counter session](contracts/counter_session.py) imports the counter's Python
|
|
126
|
+
ABI to call a deployed contract. The session exports only `call`.
|
|
127
|
+
- [MyToken](contracts/my_token.py) inherits the [CEP-18 base](contracts/cep18.py),
|
|
128
|
+
including its entrypoints, permissions and typed events.
|
|
129
|
+
- [MyNft](contracts/my_nft.py) inherits the [CEP-78 base](contracts/cep78.py),
|
|
130
|
+
with NFT metadata, approvals, ownership receipts and typed events.
|
|
131
|
+
- [Basic](contracts/basic.py) shows the procedural `contract_api` beneath the
|
|
132
|
+
class API.
|
|
133
|
+
|
|
134
|
+
## Testing
|
|
135
|
+
|
|
136
|
+
Use pytest or unittest with `MockHost` for fast feedback. `ContractHarness` adds
|
|
137
|
+
argument decoding, contract context, return encoding and transaction rollback.
|
|
138
|
+
Compiled tests exercise the actual MicroPython/C adapter in Wasmtime. Live tests
|
|
139
|
+
start and stop their own Casper devnets.
|
|
140
|
+
|
|
141
|
+
```sh
|
|
142
|
+
make test # Native Python tests
|
|
143
|
+
make typecheck
|
|
144
|
+
make build # Example Wasm artifacts needed by the compiled suite
|
|
145
|
+
make test-wasm
|
|
146
|
+
make test-live # Starts local Casper networks; requires casper-devnet
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
`casthon test` is a pytest wrapper. Wasm and live tests are ordinary test suites,
|
|
150
|
+
with the live opt-in described in the [testing guide](docs/testing.md).
|
|
151
|
+
|
|
152
|
+
## Tooling benchmarks
|
|
153
|
+
|
|
154
|
+
Matching [counter contracts and six tests](benchmarks/tooling/), measured on
|
|
155
|
+
2026-10-22 with an Apple M1 Max (10 cores, 64 GB RAM, macOS 27.0).
|
|
156
|
+
Times are **median wall-clock seconds across three runs**, including command
|
|
157
|
+
startup. The comparison uses Casthon 0.1.0, Odra 2.9.1 and cargo-odra 0.1.8.
|
|
158
|
+
|
|
159
|
+
| Workflow | Casthon (seconds) | Odra (seconds) |
|
|
160
|
+
| --- | ---: | ---: |
|
|
161
|
+
| Clean release build | 44.59 | 26.20 |
|
|
162
|
+
| First unchanged build after clean | 15.98 | 0.40 |
|
|
163
|
+
| Cached unchanged build | 1.19 | 0.30 |
|
|
164
|
+
| Incremental build after a source edit | 5.14 | 2.57 |
|
|
165
|
+
| Cold check / Clippy | 0.92 | 33.74 |
|
|
166
|
+
| Cached unchanged check | 0.17 | 0.28 |
|
|
167
|
+
| Incremental check after a source edit | 0.28 | 0.29 |
|
|
168
|
+
| Cold tests | 0.36 | 153.20 |
|
|
169
|
+
| Warm unchanged tests | 0.18 | 0.43 |
|
|
170
|
+
| Tests after a source edit | 0.19 | 1.05 |
|
|
171
|
+
|
|
172
|
+
Clean/cold runs clear compilation or checker caches; tool installation and
|
|
173
|
+
downloads are excluded. Incremental runs change an entrypoint's return value
|
|
174
|
+
from `1` to `2`. Builds use each tool's standard release pipeline; Casthon also
|
|
175
|
+
produces validated, metered Wasm. Checks compare `casthon check contracts` with
|
|
176
|
+
default-target `cargo clippy --locked -- -D warnings`. Tests run the six matching
|
|
177
|
+
cases through Casthon's native `MockHost`/`ContractHarness` and OdraVM, including
|
|
178
|
+
any required test compilation.
|
|
179
|
+
|
|
180
|
+
The first unchanged Casthon build in this snapshot recompiles the runtime:
|
|
181
|
+
Emscripten's one-time sanity-check message changes the compiler cache key.
|
|
182
|
+
Later unchanged builds hit the cache. Other sessions were active and OS file
|
|
183
|
+
caches were retained, so these figures are indicative; Odra's cold test runs
|
|
184
|
+
ranged from 150 to 251 seconds.
|
|
185
|
+
|
|
186
|
+
To repeat the comparison, install cargo-odra 0.1.8 and the required toolchains,
|
|
187
|
+
then run the [benchmark script](scripts/benchmark_tooling.py):
|
|
188
|
+
|
|
189
|
+
```sh
|
|
190
|
+
.venv/bin/python scripts/benchmark_tooling.py --repeats 3
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
The runner isolates its builds and defaults to saving reports and raw logs under
|
|
194
|
+
the ignored `.experiments/tooling-benchmarks/` directory. Use
|
|
195
|
+
`--clippy-all-targets` to include test targets in the Clippy comparison.
|
|
196
|
+
|
|
197
|
+
## Read more
|
|
198
|
+
|
|
199
|
+
| Guide | Contents |
|
|
200
|
+
| --- | --- |
|
|
201
|
+
| [CLI](docs/casthon.md) | Setup, configuration, commands and artifacts |
|
|
202
|
+
| [Python API](docs/python-api.md) | Classes, typed arguments, state, events and low-level calls |
|
|
203
|
+
| [Testing](docs/testing.md) | Mock state, invocation boundaries and live networks |
|
|
204
|
+
| [CEP-18](docs/cep18.md) | Token implementation, event formats and upgrades |
|
|
205
|
+
| [CEP-78](docs/cep78.md) | NFT collections, metadata, approvals, receipts and Python upgrades |
|
|
206
|
+
| [Repository and architecture](docs/architecture.md) | Code ownership, build stages and the remaining 0.1 work |
|
|
207
|
+
| [Short pitch](docs/pitch.md) | A brief introduction and a small demo |
|
|
208
|
+
| [Native port](native/README.md) | Interpreter lifecycle, unwinding and host completion |
|
|
209
|
+
|
|
210
|
+
## Status
|
|
211
|
+
|
|
212
|
+
Casthon is a developer preview. The CLI and SDK live in one repository; the CLI
|
|
213
|
+
still needs this checkout's compiler and runtime resources. Standalone packaging
|
|
214
|
+
and selecting an installed project SDK are the next release work.
|
|
215
|
+
|
|
216
|
+
The default runtime is pristine MicroPython 1.29.0 with Asyncify, software binary64
|
|
217
|
+
floats, and fixed 4 MiB linear memory with GC compiled out. The preserved `native-errors`
|
|
218
|
+
backend remains available. Supported Python constructs and runtime limits are
|
|
219
|
+
listed in the [architecture guide](docs/architecture.md#runtime-profile).
|
|
220
|
+
|
|
221
|
+
[Logo credits](docs/assets/README.md).
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="docs/assets/casthon-logo.png" width="176" alt="A friendly ghost with a curled tail, inspired by the snake emoji">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
# Casthon
|
|
6
|
+
|
|
7
|
+
**Python smart contracts for Casper.**
|
|
8
|
+
|
|
9
|
+
Write a contract as a Python class, give its entrypoints type annotations, and
|
|
10
|
+
build it into Casper-compatible WebAssembly. Casthon freezes your code with
|
|
11
|
+
MicroPython; the `casthon` SDK handles arguments, storage, return values and host calls.
|
|
12
|
+
|
|
13
|
+
The same contract source runs in ordinary Python tests. You can compose classes,
|
|
14
|
+
catch exceptions, and use `finally` for cleanup. An uncaught contract error becomes
|
|
15
|
+
a Casper revert at the outer invocation boundary.
|
|
16
|
+
|
|
17
|
+
## Hello, Casper
|
|
18
|
+
|
|
19
|
+
Here is [hello.py](contracts/hello.py), including its installer:
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from casthon import ContractHash, ContractPackageHash, U32, URef, UserError, export
|
|
23
|
+
from casthon.contract_api import storage
|
|
24
|
+
from casthon.contract_api.runtime import named_keys
|
|
25
|
+
from casthon.contracts import Contract, entry_point, export_contract
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@export_contract
|
|
29
|
+
class Hello(Contract):
|
|
30
|
+
calls: URef[U32]
|
|
31
|
+
|
|
32
|
+
@entry_point
|
|
33
|
+
def greet(self, name: str) -> str:
|
|
34
|
+
try:
|
|
35
|
+
name = name.strip()
|
|
36
|
+
if not name:
|
|
37
|
+
raise ValueError("Empty name")
|
|
38
|
+
if len(name) > 64:
|
|
39
|
+
raise UserError(1)
|
|
40
|
+
return f"Hello, {name}!"
|
|
41
|
+
except ValueError:
|
|
42
|
+
return "Hello, stranger!"
|
|
43
|
+
finally:
|
|
44
|
+
self.calls.write(self.calls.read() + 1)
|
|
45
|
+
print("Greeting finished")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
installed = named_keys.reference("hello", ContractHash)
|
|
49
|
+
installed_package = named_keys.reference("hello_package", ContractPackageHash)
|
|
50
|
+
installed_access = named_keys.reference("hello_access", URef[None])
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@export
|
|
54
|
+
def call() -> None:
|
|
55
|
+
package, access = storage.create_contract_package_at_hash()
|
|
56
|
+
contract, _ = package.add_contract_version(Hello(calls=0))
|
|
57
|
+
installed.set(contract)
|
|
58
|
+
installed_package.set(package)
|
|
59
|
+
installed_access.set(access)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`greet` receives and returns a Python string. `calls` is persistent U32 state.
|
|
63
|
+
An empty name takes the `except` branch. A name longer than 64 characters raises
|
|
64
|
+
a user error and reverts. The `finally` block runs in either case; a revert rolls
|
|
65
|
+
back its storage write too.
|
|
66
|
+
|
|
67
|
+
`@entry_point` describes the class ABI. `@export_contract` selects its Wasm
|
|
68
|
+
exports. The top-level `call` creates the package and stores the deployed hashes
|
|
69
|
+
explicitly. [The Python API guide](docs/python-api.md) explains the boundaries.
|
|
70
|
+
|
|
71
|
+
## Try it
|
|
72
|
+
|
|
73
|
+
This developer preview builds from a source checkout. You need Python 3.11+,
|
|
74
|
+
`uv`, Rust/Cargo, Node, Make, a host C compiler, Emscripten and Binaryen.
|
|
75
|
+
The [CLI guide](docs/casthon.md#requirements) covers the build requirements.
|
|
76
|
+
|
|
77
|
+
```sh
|
|
78
|
+
make setup
|
|
79
|
+
make cli
|
|
80
|
+
./target/release/casthon doctor
|
|
81
|
+
./target/release/casthon test -q
|
|
82
|
+
./target/release/casthon build hello
|
|
83
|
+
./target/release/casthon inspect build/hello.wasm
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The build produces `build/hello.wasm`, its metered test counterpart, and a build
|
|
87
|
+
report. It compiles the contract; deployment is a separate transaction.
|
|
88
|
+
|
|
89
|
+
Debug builds keep `print()` and exception tracebacks. For a release build:
|
|
90
|
+
|
|
91
|
+
```sh
|
|
92
|
+
./target/release/casthon build hello --release
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
That writes `build/release/hello.wasm` and removes the `casper_print` import.
|
|
96
|
+
See [CLI usage](docs/casthon.md) for project configuration, templates, caching
|
|
97
|
+
and minification.
|
|
98
|
+
|
|
99
|
+
## A few more examples
|
|
100
|
+
|
|
101
|
+
- [Counter](contracts/counter.py) composes ownership and pause controls, with a
|
|
102
|
+
typed URef and a persistent dictionary.
|
|
103
|
+
- [Counter session](contracts/counter_session.py) imports the counter's Python
|
|
104
|
+
ABI to call a deployed contract. The session exports only `call`.
|
|
105
|
+
- [MyToken](contracts/my_token.py) inherits the [CEP-18 base](contracts/cep18.py),
|
|
106
|
+
including its entrypoints, permissions and typed events.
|
|
107
|
+
- [MyNft](contracts/my_nft.py) inherits the [CEP-78 base](contracts/cep78.py),
|
|
108
|
+
with NFT metadata, approvals, ownership receipts and typed events.
|
|
109
|
+
- [Basic](contracts/basic.py) shows the procedural `contract_api` beneath the
|
|
110
|
+
class API.
|
|
111
|
+
|
|
112
|
+
## Testing
|
|
113
|
+
|
|
114
|
+
Use pytest or unittest with `MockHost` for fast feedback. `ContractHarness` adds
|
|
115
|
+
argument decoding, contract context, return encoding and transaction rollback.
|
|
116
|
+
Compiled tests exercise the actual MicroPython/C adapter in Wasmtime. Live tests
|
|
117
|
+
start and stop their own Casper devnets.
|
|
118
|
+
|
|
119
|
+
```sh
|
|
120
|
+
make test # Native Python tests
|
|
121
|
+
make typecheck
|
|
122
|
+
make build # Example Wasm artifacts needed by the compiled suite
|
|
123
|
+
make test-wasm
|
|
124
|
+
make test-live # Starts local Casper networks; requires casper-devnet
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`casthon test` is a pytest wrapper. Wasm and live tests are ordinary test suites,
|
|
128
|
+
with the live opt-in described in the [testing guide](docs/testing.md).
|
|
129
|
+
|
|
130
|
+
## Tooling benchmarks
|
|
131
|
+
|
|
132
|
+
Matching [counter contracts and six tests](benchmarks/tooling/), measured on
|
|
133
|
+
2026-10-22 with an Apple M1 Max (10 cores, 64 GB RAM, macOS 27.0).
|
|
134
|
+
Times are **median wall-clock seconds across three runs**, including command
|
|
135
|
+
startup. The comparison uses Casthon 0.1.0, Odra 2.9.1 and cargo-odra 0.1.8.
|
|
136
|
+
|
|
137
|
+
| Workflow | Casthon (seconds) | Odra (seconds) |
|
|
138
|
+
| --- | ---: | ---: |
|
|
139
|
+
| Clean release build | 44.59 | 26.20 |
|
|
140
|
+
| First unchanged build after clean | 15.98 | 0.40 |
|
|
141
|
+
| Cached unchanged build | 1.19 | 0.30 |
|
|
142
|
+
| Incremental build after a source edit | 5.14 | 2.57 |
|
|
143
|
+
| Cold check / Clippy | 0.92 | 33.74 |
|
|
144
|
+
| Cached unchanged check | 0.17 | 0.28 |
|
|
145
|
+
| Incremental check after a source edit | 0.28 | 0.29 |
|
|
146
|
+
| Cold tests | 0.36 | 153.20 |
|
|
147
|
+
| Warm unchanged tests | 0.18 | 0.43 |
|
|
148
|
+
| Tests after a source edit | 0.19 | 1.05 |
|
|
149
|
+
|
|
150
|
+
Clean/cold runs clear compilation or checker caches; tool installation and
|
|
151
|
+
downloads are excluded. Incremental runs change an entrypoint's return value
|
|
152
|
+
from `1` to `2`. Builds use each tool's standard release pipeline; Casthon also
|
|
153
|
+
produces validated, metered Wasm. Checks compare `casthon check contracts` with
|
|
154
|
+
default-target `cargo clippy --locked -- -D warnings`. Tests run the six matching
|
|
155
|
+
cases through Casthon's native `MockHost`/`ContractHarness` and OdraVM, including
|
|
156
|
+
any required test compilation.
|
|
157
|
+
|
|
158
|
+
The first unchanged Casthon build in this snapshot recompiles the runtime:
|
|
159
|
+
Emscripten's one-time sanity-check message changes the compiler cache key.
|
|
160
|
+
Later unchanged builds hit the cache. Other sessions were active and OS file
|
|
161
|
+
caches were retained, so these figures are indicative; Odra's cold test runs
|
|
162
|
+
ranged from 150 to 251 seconds.
|
|
163
|
+
|
|
164
|
+
To repeat the comparison, install cargo-odra 0.1.8 and the required toolchains,
|
|
165
|
+
then run the [benchmark script](scripts/benchmark_tooling.py):
|
|
166
|
+
|
|
167
|
+
```sh
|
|
168
|
+
.venv/bin/python scripts/benchmark_tooling.py --repeats 3
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
The runner isolates its builds and defaults to saving reports and raw logs under
|
|
172
|
+
the ignored `.experiments/tooling-benchmarks/` directory. Use
|
|
173
|
+
`--clippy-all-targets` to include test targets in the Clippy comparison.
|
|
174
|
+
|
|
175
|
+
## Read more
|
|
176
|
+
|
|
177
|
+
| Guide | Contents |
|
|
178
|
+
| --- | --- |
|
|
179
|
+
| [CLI](docs/casthon.md) | Setup, configuration, commands and artifacts |
|
|
180
|
+
| [Python API](docs/python-api.md) | Classes, typed arguments, state, events and low-level calls |
|
|
181
|
+
| [Testing](docs/testing.md) | Mock state, invocation boundaries and live networks |
|
|
182
|
+
| [CEP-18](docs/cep18.md) | Token implementation, event formats and upgrades |
|
|
183
|
+
| [CEP-78](docs/cep78.md) | NFT collections, metadata, approvals, receipts and Python upgrades |
|
|
184
|
+
| [Repository and architecture](docs/architecture.md) | Code ownership, build stages and the remaining 0.1 work |
|
|
185
|
+
| [Short pitch](docs/pitch.md) | A brief introduction and a small demo |
|
|
186
|
+
| [Native port](native/README.md) | Interpreter lifecycle, unwinding and host completion |
|
|
187
|
+
|
|
188
|
+
## Status
|
|
189
|
+
|
|
190
|
+
Casthon is a developer preview. The CLI and SDK live in one repository; the CLI
|
|
191
|
+
still needs this checkout's compiler and runtime resources. Standalone packaging
|
|
192
|
+
and selecting an installed project SDK are the next release work.
|
|
193
|
+
|
|
194
|
+
The default runtime is pristine MicroPython 1.29.0 with Asyncify, software binary64
|
|
195
|
+
floats, and fixed 4 MiB linear memory with GC compiled out. The preserved `native-errors`
|
|
196
|
+
backend remains available. Supported Python constructs and runtime limits are
|
|
197
|
+
listed in the [architecture guide](docs/architecture.md#runtime-profile).
|
|
198
|
+
|
|
199
|
+
[Logo credits](docs/assets/README.md).
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=75"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "casthon-sdk"
|
|
7
|
+
version = "0.1.0a1"
|
|
8
|
+
description = "Python SDK for writing Casper smart contracts with Casthon"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
dependencies = ["typing_extensions>=4.12; python_version<'3.12'"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 3 - Alpha",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3.11",
|
|
17
|
+
"Programming Language :: Python :: 3.12",
|
|
18
|
+
"Programming Language :: Python :: 3.13",
|
|
19
|
+
"Programming Language :: Python :: 3.14",
|
|
20
|
+
"Typing :: Typed",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Repository = "https://github.com/veles-labs/casthon"
|
|
25
|
+
Issues = "https://github.com/veles-labs/casthon/issues"
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
dev = ["mypy==1.19.1", "wasmtime==49.0.0", "pytest==9.1.1"]
|
|
29
|
+
|
|
30
|
+
[tool.casthon]
|
|
31
|
+
micropython = "1.29.0"
|
|
32
|
+
targets = ["contracts/counter.py"]
|
|
33
|
+
|
|
34
|
+
[tool.pytest.ini_options]
|
|
35
|
+
testpaths = ["tests/test_*.py"]
|
|
36
|
+
pythonpath = ["src", "tests", "tests/wasm", "tests/fixtures"]
|
|
37
|
+
addopts = ["--import-mode=importlib"]
|
|
38
|
+
|
|
39
|
+
[tool.setuptools.packages.find]
|
|
40
|
+
where = ["src"]
|
|
41
|
+
|
|
42
|
+
[tool.mypy]
|
|
43
|
+
python_version = "3.11"
|
|
44
|
+
strict = true
|
|
45
|
+
mypy_path = "src"
|
|
46
|
+
files = ["contracts", "src/casthon"]
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Casper data types; host functions live in casthon.contract_api."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable, TypeVar
|
|
4
|
+
|
|
5
|
+
from . import api_error as api_error
|
|
6
|
+
|
|
7
|
+
from .errors import (
|
|
8
|
+
Ret as Ret, Revert as Revert, ApiError as ApiError, User as User, UserError as UserError,
|
|
9
|
+
MissingKey as MissingKey, MissingArgument as MissingArgument,
|
|
10
|
+
InvalidArgument as InvalidArgument, InvalidPurse as InvalidPurse,
|
|
11
|
+
Read as Read, ValueNotFound as ValueNotFound,
|
|
12
|
+
DictionaryItemKeyExceedsLength as DictionaryItemKeyExceedsLength,
|
|
13
|
+
)
|
|
14
|
+
from .key import (
|
|
15
|
+
Key as Key,
|
|
16
|
+
AccountHash as AccountHash,
|
|
17
|
+
Hash as Hash,
|
|
18
|
+
ContractHash as ContractHash,
|
|
19
|
+
ContractPackageHash as ContractPackageHash,
|
|
20
|
+
URef as URef,
|
|
21
|
+
TransferHash as TransferHash,
|
|
22
|
+
DeployInfo as DeployInfo,
|
|
23
|
+
EraInfo as EraInfo,
|
|
24
|
+
Balance as Balance,
|
|
25
|
+
Bid as Bid,
|
|
26
|
+
Withdraw as Withdraw,
|
|
27
|
+
DictionaryAddress as DictionaryAddress,
|
|
28
|
+
SystemEntityRegistry as SystemEntityRegistry,
|
|
29
|
+
EraSummary as EraSummary,
|
|
30
|
+
Unbond as Unbond,
|
|
31
|
+
ChainspecRegistry as ChainspecRegistry,
|
|
32
|
+
ChecksumRegistry as ChecksumRegistry,
|
|
33
|
+
BidAddress as BidAddress,
|
|
34
|
+
PackageHash as PackageHash,
|
|
35
|
+
AddressableEntity as AddressableEntity,
|
|
36
|
+
SystemEntity as SystemEntity,
|
|
37
|
+
AccountEntity as AccountEntity,
|
|
38
|
+
ContractEntity as ContractEntity,
|
|
39
|
+
ByteCode as ByteCode,
|
|
40
|
+
Message as Message,
|
|
41
|
+
NamedKeyAddress as NamedKeyAddress,
|
|
42
|
+
BlockGlobal as BlockGlobal,
|
|
43
|
+
BalanceHold as BalanceHold,
|
|
44
|
+
EntryPointAddress as EntryPointAddress,
|
|
45
|
+
State as State,
|
|
46
|
+
base64 as base64,
|
|
47
|
+
)
|
|
48
|
+
from .enums import (
|
|
49
|
+
WireEnum as WireEnum,
|
|
50
|
+
Phase as Phase, ActionType as ActionType, AddKeyFailure as AddKeyFailure,
|
|
51
|
+
RemoveKeyFailure as RemoveKeyFailure, UpdateKeyFailure as UpdateKeyFailure,
|
|
52
|
+
SetThresholdFailure as SetThresholdFailure, SystemEntityType as SystemEntityType,
|
|
53
|
+
TransferredTo as TransferredTo, HashAlgorithm as HashAlgorithm,
|
|
54
|
+
MessageTopicOperation as MessageTopicOperation, EntryPointAccess as EntryPointAccess,
|
|
55
|
+
EntryPointType as EntryPointType, EntryPointPayment as EntryPointPayment,
|
|
56
|
+
)
|
|
57
|
+
from .types import (
|
|
58
|
+
Map as Map,
|
|
59
|
+
CLType as CLType,
|
|
60
|
+
cl_type_of as cl_type_of,
|
|
61
|
+
U8 as U8,
|
|
62
|
+
U32 as U32,
|
|
63
|
+
U64 as U64,
|
|
64
|
+
U256 as U256,
|
|
65
|
+
U512 as U512,
|
|
66
|
+
wire_type as wire_type,
|
|
67
|
+
)
|
|
68
|
+
from .codec import ABIError as ABIError
|
|
69
|
+
|
|
70
|
+
from .entry_points import EntryPoint as EntryPoint
|
|
71
|
+
|
|
72
|
+
from .values import (
|
|
73
|
+
CLValue as CLValue,
|
|
74
|
+
CLU8 as CLU8,
|
|
75
|
+
CLU32 as CLU32,
|
|
76
|
+
CLU64 as CLU64,
|
|
77
|
+
CLU256 as CLU256,
|
|
78
|
+
CLU512 as CLU512,
|
|
79
|
+
CLString as CLString,
|
|
80
|
+
CLBool as CLBool,
|
|
81
|
+
CLKey as CLKey,
|
|
82
|
+
CLKeyList as CLKeyList,
|
|
83
|
+
CLSecurityMap as CLSecurityMap,
|
|
84
|
+
CLBytes as CLBytes,
|
|
85
|
+
CLUnit as CLUnit,
|
|
86
|
+
SerializedCLValue as SerializedCLValue,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
from .arguments import OptionalArg as OptionalArg, MISSING as MISSING, MissingType as MissingType
|
|
90
|
+
from .references import Dictionary as Dictionary
|
|
91
|
+
from .crypto import (
|
|
92
|
+
PublicKey as PublicKey, SystemPublicKey as SystemPublicKey,
|
|
93
|
+
Ed25519PublicKey as Ed25519PublicKey, Secp256k1PublicKey as Secp256k1PublicKey,
|
|
94
|
+
Signature as Signature, Ed25519Signature as Ed25519Signature,
|
|
95
|
+
Secp256k1Signature as Secp256k1Signature,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
from .messages import MessagePayload as MessagePayload, MessageString as MessageString, MessageBytes as MessageBytes
|
|
99
|
+
from .caller import (
|
|
100
|
+
CallerInfo as CallerInfo, AccountCaller as AccountCaller, EntityCaller as EntityCaller,
|
|
101
|
+
ContractCaller as ContractCaller, CallStackElement as CallStackElement, Session as Session,
|
|
102
|
+
StoredSession as StoredSession, StoredContract as StoredContract,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
_ExportFunction = TypeVar("_ExportFunction", bound=Callable[..., Any])
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def export(function: _ExportFunction) -> _ExportFunction:
|
|
110
|
+
"""Select a Wasm export; annotations apply only at its invocation boundary."""
|
|
111
|
+
return function
|
|
112
|
+
|
|
113
|
+
from .events import (
|
|
114
|
+
Event as Event, MessageEncoder as MessageEncoder, Bytesrepr as Bytesrepr,
|
|
115
|
+
Json as Json, JsonBytes as JsonBytes, emit as emit,
|
|
116
|
+
)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Structural CL types shared by native Python and frozen contracts."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
from .key import Key
|
|
5
|
+
from . import key as _key_types
|
|
6
|
+
from ._wire_types import _Unsigned, _Optional, _EnumRef, enum_type
|
|
7
|
+
|
|
8
|
+
NAMES = {0: "Bool", 3: "U8", 4: "U32", 5: "U64", 7: "U256", 8: "U512",
|
|
9
|
+
9: "Unit", 10: "String", 11: "Key", 12: "URef", 13: "Option", 14: "List", 17: "Map",
|
|
10
|
+
18: "Tuple1", 19: "Tuple2", 20: "Tuple3"}
|
|
11
|
+
BITS = {3: 8, 4: 32, 5: 64, 7: 256, 8: 512}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CLType:
|
|
15
|
+
def __init__(self, tag: int, children: tuple[Any, ...] = (), annotation: Any = None) -> None:
|
|
16
|
+
if tag not in NAMES or len(children) != {13: 1, 14: 1, 17: 2, 18: 1, 19: 2, 20: 3}.get(tag, 0):
|
|
17
|
+
raise TypeError("Unsupported CL type descriptor")
|
|
18
|
+
self.tag, self.annotation = tag, annotation
|
|
19
|
+
self.depth = 0
|
|
20
|
+
resolved = []
|
|
21
|
+
for child in children:
|
|
22
|
+
child = child if isinstance(child, CLType) else resolve_type(child)
|
|
23
|
+
self.depth = max(self.depth, child.depth + 1)
|
|
24
|
+
if self.depth > 32:
|
|
25
|
+
raise TypeError("CL type nesting limit")
|
|
26
|
+
resolved.append(child)
|
|
27
|
+
self.children = tuple(resolved)
|
|
28
|
+
|
|
29
|
+
def __str__(self) -> str:
|
|
30
|
+
if self.annotation is bytes:
|
|
31
|
+
return "Bytes"
|
|
32
|
+
name = NAMES[self.tag]
|
|
33
|
+
if not self.children:
|
|
34
|
+
return name
|
|
35
|
+
parts = []
|
|
36
|
+
for child in self.children:
|
|
37
|
+
parts.append(str(child))
|
|
38
|
+
return name + "<" + ",".join(parts) + ">"
|
|
39
|
+
|
|
40
|
+
def __repr__(self) -> str:
|
|
41
|
+
return str(self)
|
|
42
|
+
|
|
43
|
+
def to_bytes(self) -> bytes:
|
|
44
|
+
data = bytes([self.tag])
|
|
45
|
+
for child in self.children:
|
|
46
|
+
data += child.to_bytes()
|
|
47
|
+
return data
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
SCALARS = {}
|
|
51
|
+
for _tag, _name in NAMES.items():
|
|
52
|
+
if _tag not in (13, 14, 17, 18, 19, 20):
|
|
53
|
+
SCALARS[_name] = CLType(_tag)
|
|
54
|
+
SCALARS["Key"].annotation = Key
|
|
55
|
+
SCALARS["Bytes"] = CLType(14, (SCALARS["U8"],), bytes)
|
|
56
|
+
# Key variants are SDK types known at startup. Reuse their descriptors rather
|
|
57
|
+
# than constructing objects underneath deeply nested contract imports.
|
|
58
|
+
KEY_TYPES = {Key: SCALARS["Key"]}
|
|
59
|
+
for _name in dir(_key_types):
|
|
60
|
+
_variant = getattr(_key_types, _name)
|
|
61
|
+
if isinstance(_variant, type) and _variant is not Key and issubclass(_variant, Key):
|
|
62
|
+
KEY_TYPES[_variant] = CLType(11, annotation=_variant)
|
|
63
|
+
|
|
64
|
+
# Compatibility for existing low-level callers. New type expressions never
|
|
65
|
+
# stringify their children and do not use this legacy-name boundary.
|
|
66
|
+
LEGACY = SCALARS.copy()
|
|
67
|
+
LEGACY["List<Key>"] = CLType(14, (SCALARS["Key"],))
|
|
68
|
+
LEGACY["Map<Key,U8>"] = CLType(17, (SCALARS["Key"], SCALARS["U8"]))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def resolve_type(annotation: Any, depth: int = 0) -> CLType:
|
|
72
|
+
if depth > 32:
|
|
73
|
+
raise TypeError("CL type nesting limit")
|
|
74
|
+
if isinstance(annotation, CLType):
|
|
75
|
+
return annotation
|
|
76
|
+
# A typed URef used as a wire value has Casper's URef CLType. Bare Key
|
|
77
|
+
# subclasses, including bare URef, retain their existing Key encoding.
|
|
78
|
+
from .references import URefType
|
|
79
|
+
if isinstance(annotation, URefType):
|
|
80
|
+
return CLType(12, annotation=annotation)
|
|
81
|
+
if isinstance(annotation, _Unsigned):
|
|
82
|
+
return SCALARS[annotation.kind]
|
|
83
|
+
if isinstance(annotation, _Optional):
|
|
84
|
+
return CLType(13, (resolve_type(annotation.annotation, depth + 1),))
|
|
85
|
+
if isinstance(annotation, _EnumRef) or enum_type(annotation) is not None:
|
|
86
|
+
return CLType(3, annotation=annotation)
|
|
87
|
+
if isinstance(annotation, type) and issubclass(annotation, Key):
|
|
88
|
+
if annotation not in KEY_TYPES:
|
|
89
|
+
KEY_TYPES[annotation] = CLType(11, annotation=annotation)
|
|
90
|
+
return KEY_TYPES[annotation]
|
|
91
|
+
if annotation is bytes:
|
|
92
|
+
return SCALARS["Bytes"]
|
|
93
|
+
if annotation is bool:
|
|
94
|
+
return SCALARS["Bool"]
|
|
95
|
+
if annotation is str:
|
|
96
|
+
return SCALARS["String"]
|
|
97
|
+
if annotation is None or annotation is type(None):
|
|
98
|
+
return SCALARS["Unit"]
|
|
99
|
+
if type(annotation) is str:
|
|
100
|
+
if annotation in LEGACY:
|
|
101
|
+
return LEGACY[annotation]
|
|
102
|
+
if annotation.startswith("Option<") and annotation.endswith(">"):
|
|
103
|
+
return CLType(13, (resolve_type(annotation[7:-1], depth + 1),))
|
|
104
|
+
raise TypeError("Use an explicit Casper wire type: " + str(annotation))
|