quantufai 0.1.0__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.
- quantufai-0.1.0/PKG-INFO +154 -0
- quantufai-0.1.0/README.md +143 -0
- quantufai-0.1.0/pyproject.toml +31 -0
- quantufai-0.1.0/quantufai/__init__.py +68 -0
- quantufai-0.1.0/quantufai/_version.py +1 -0
- quantufai-0.1.0/quantufai/canonical.py +100 -0
- quantufai-0.1.0/quantufai/client.py +409 -0
- quantufai-0.1.0/quantufai/types.py +427 -0
- quantufai-0.1.0/quantufai/verify.py +196 -0
- quantufai-0.1.0/quantufai.egg-info/PKG-INFO +154 -0
- quantufai-0.1.0/quantufai.egg-info/SOURCES.txt +17 -0
- quantufai-0.1.0/quantufai.egg-info/dependency_links.txt +1 -0
- quantufai-0.1.0/quantufai.egg-info/requires.txt +3 -0
- quantufai-0.1.0/quantufai.egg-info/top_level.txt +1 -0
- quantufai-0.1.0/setup.cfg +4 -0
- quantufai-0.1.0/tests/test_canonical.py +54 -0
- quantufai-0.1.0/tests/test_client.py +178 -0
- quantufai-0.1.0/tests/test_no_spend_surface.py +99 -0
- quantufai-0.1.0/tests/test_verify.py +97 -0
quantufai-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: quantufai
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: QuantufAI developer SDK: quote quantum runs before any spend, read job status/results/receipts, export circuits, and simulate on the free sandbox — this SDK can price and read, it can never spend.
|
|
5
|
+
Author: QuantufAI, Inc.
|
|
6
|
+
License: Proprietary
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Provides-Extra: verify
|
|
10
|
+
Requires-Dist: cryptography>=41; extra == "verify"
|
|
11
|
+
|
|
12
|
+
# quantufai — the QuantufAI Python SDK
|
|
13
|
+
|
|
14
|
+
Quantum compute a program can **price and read — never spend**.
|
|
15
|
+
|
|
16
|
+
> ⚠️ **Publish gate:** this package is repo-code only. It is **not** on PyPI
|
|
17
|
+
> and must not be published or linked from public docs until the founder
|
|
18
|
+
> confirms counsel's provisional filing. Local installs are fine:
|
|
19
|
+
> `pip install -e sdks/python`.
|
|
20
|
+
|
|
21
|
+
## What this SDK can do
|
|
22
|
+
|
|
23
|
+
| You want | Call | Key scope |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| "What would this run cost?" | `client.quote(circuit, shots=...)` | `quotes:read` |
|
|
26
|
+
| "Is it done yet?" | `client.job_status(job_id)` / `client.wait(job_id)` | `jobs:read` |
|
|
27
|
+
| Counts + ledger + error bars, verbatim | `client.job_result(job_id)` | `jobs:read` |
|
|
28
|
+
| The signed, tamper-evident receipt | `client.governed_receipt(job_id)` | `jobs:read` |
|
|
29
|
+
| Check a receipt you hold | `client.verify_receipt(receipt)` | `jobs:read` (platform tier) / none (offline tier) |
|
|
30
|
+
| The exact circuit that ran, as Qiskit/Cirq/Braket/pytket/QASM | `client.export_circuit(job_id, format=..., which=...)` | `results:export` |
|
|
31
|
+
| Download the result artifact | `client.export_result(job_id)` | `results:export` |
|
|
32
|
+
| Run on the **free local simulator** ($0) | `client.sandbox_simulate(circuit)` | `runs:simulate` (sandbox key) |
|
|
33
|
+
| What can this key do? | `client.me()` | `account:read` |
|
|
34
|
+
|
|
35
|
+
## What this SDK cannot do — by design, not omission
|
|
36
|
+
|
|
37
|
+
**There is no dispatch method. There is no approval method. There is no
|
|
38
|
+
billing method.** Spending money on quantum hardware requires a human
|
|
39
|
+
approving a signed quote in the QuantufAI dashboard:
|
|
40
|
+
|
|
41
|
+
- The platform enforces quote-before-spend server-side; scoped API keys
|
|
42
|
+
cannot approve a spend.
|
|
43
|
+
- On top of that, this SDK's one execution method (`sandbox_simulate`) pins
|
|
44
|
+
every request to the free local simulator (`preferredProviders:
|
|
45
|
+
["classical"]`), so **even a key carrying the paid `runs:execute` scope
|
|
46
|
+
cannot reach billable hardware through this SDK**. Zero eligible providers
|
|
47
|
+
is a typed failure on the platform — never a silent reroute.
|
|
48
|
+
|
|
49
|
+
An AI agent driving this client can tell you exactly what an experiment would
|
|
50
|
+
cost and read every receipt — and cannot buy anything.
|
|
51
|
+
|
|
52
|
+
## REFUSED is a status, not an exception
|
|
53
|
+
|
|
54
|
+
Every deliberate platform refusal — missing scope, sandbox clamp, someone
|
|
55
|
+
else's job, quota exhausted, unprovable circuit translation — comes back as a
|
|
56
|
+
typed `Refusal` with the platform's `code`, `message`, and `details`
|
|
57
|
+
verbatim:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
result = client.job_result("not-my-job")
|
|
61
|
+
if isinstance(result, quantufai.Refusal):
|
|
62
|
+
print(result.status) # "REFUSED"
|
|
63
|
+
print(result.code) # e.g. "job_not_found", "insufficient_scope"
|
|
64
|
+
print(result.details) # e.g. {"requiredScope": "jobs:read", ...}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Exceptions are reserved for transport failures (`TransportError`) and
|
|
68
|
+
unexpected 5xx answers (`PlatformError`, body preserved).
|
|
69
|
+
|
|
70
|
+
## Quickstart (sandbox — no card, $0)
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
import quantufai
|
|
74
|
+
|
|
75
|
+
client = quantufai.Client(
|
|
76
|
+
api_key="qfai_sk_...", # or $QUANTUFAI_API_KEY; sandbox tier: mint
|
|
77
|
+
# with {"tier": "sandbox"} in the dashboard
|
|
78
|
+
base_url="https://YOUR_HOST", # or $QUANTUFAI_BASE_URL
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
bell = """OPENQASM 2.0;
|
|
82
|
+
include "qelib1.inc";
|
|
83
|
+
qreg q[2]; creg c[2];
|
|
84
|
+
h q[0]; cx q[0], q[1];
|
|
85
|
+
measure q -> c;"""
|
|
86
|
+
|
|
87
|
+
# 1. Price it first — nothing is reserved, charged, or dispatched by quoting.
|
|
88
|
+
quotes = client.quote(bell, shots=1000, qubits=2)
|
|
89
|
+
if isinstance(quotes, quantufai.Refusal):
|
|
90
|
+
raise SystemExit(f"refused: {quotes.code} — {quotes.message}")
|
|
91
|
+
for q in quotes.quotes:
|
|
92
|
+
print(f"{q.provider}: ~${q.estimated_cost_usd} ({q.reasons})")
|
|
93
|
+
|
|
94
|
+
# 2. Run on the FREE local simulator (the only execution this SDK performs).
|
|
95
|
+
run = client.sandbox_simulate(bell, shots=1000, qubits=2)
|
|
96
|
+
if isinstance(run, quantufai.Refusal):
|
|
97
|
+
raise SystemExit(f"refused: {run.code} — {run.message}")
|
|
98
|
+
print(run.state, run.counts) # real statevector physics, real counts, $0
|
|
99
|
+
|
|
100
|
+
# 3. Poll (async runs), then fetch the signed receipt.
|
|
101
|
+
result = client.wait(run.job_id)
|
|
102
|
+
receipt = client.governed_receipt(run.job_id)
|
|
103
|
+
|
|
104
|
+
# 4. Verify the receipt — offline Ed25519 tier and/or the platform's check.
|
|
105
|
+
report = client.verify_receipt(receipt)
|
|
106
|
+
print(report.verified, report.offline.public_signature)
|
|
107
|
+
|
|
108
|
+
# 5. Take the exact circuit home. `which` is required — the platform refuses
|
|
109
|
+
# to guess between "original" and "as-dispatched".
|
|
110
|
+
code = client.export_circuit(run.job_id, format="qiskit", which="as-dispatched")
|
|
111
|
+
print(code.source)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Receipts: what the tiers mean
|
|
115
|
+
|
|
116
|
+
- **Publicly-verifiable tier** (`publicSignature`, Ed25519): anyone can
|
|
117
|
+
verify offline with QuantufAI's published public key —
|
|
118
|
+
`verify_receipt(receipt, public_key=..., offline_only=True)` or the
|
|
119
|
+
standalone `tools/verify-receipt.mjs`. Needs the optional extra:
|
|
120
|
+
`pip install 'quantufai[verify]'`.
|
|
121
|
+
- **Server-attested tier** (`signature`, HMAC): the platform's own
|
|
122
|
+
attestation; only the platform can check it (that's what the platform tier
|
|
123
|
+
of `verify_receipt` asks for). Older receipts carry only this tier — the
|
|
124
|
+
SDK reports that honestly instead of pretending to a verdict.
|
|
125
|
+
|
|
126
|
+
## Results are verbatim
|
|
127
|
+
|
|
128
|
+
Counts are never re-binned, error bars are never computed client-side —
|
|
129
|
+
`JobResult.error_bars` returns exactly the uncertainty fields the platform
|
|
130
|
+
attached (with their location in the payload), and returns nothing when the
|
|
131
|
+
platform attached none.
|
|
132
|
+
|
|
133
|
+
## History: this package replaces deleted fabricating stubs
|
|
134
|
+
|
|
135
|
+
The repo previously carried `sdks/python/quantufai.py` and
|
|
136
|
+
`sdks/js/quantufai.ts` — stubs that called endpoints that never existed and
|
|
137
|
+
**invented job statuses by pattern-matching chat text**. They were deleted
|
|
138
|
+
(PR #579) and a CI test keeps them deleted. This package is their honest
|
|
139
|
+
replacement: every call maps to a real, mounted, scope-gated endpoint, and
|
|
140
|
+
anything the platform refuses surfaces as a typed `REFUSED`.
|
|
141
|
+
|
|
142
|
+
## Publish checklist (post-filing — founder go required)
|
|
143
|
+
|
|
144
|
+
1. Founder confirms counsel's provisional filing covers the receipt/verifier
|
|
145
|
+
disclosures (roadmap #4 gate).
|
|
146
|
+
2. `python -m build` from `sdks/python/`; check the wheel installs clean.
|
|
147
|
+
3. Reserve/publish `quantufai` on PyPI from the org account (never a personal
|
|
148
|
+
one); enable 2FA + trusted publishing.
|
|
149
|
+
4. Only then link the SDK from `/docs` (the docs deliberately do not mention
|
|
150
|
+
it today).
|
|
151
|
+
5. Announcement claim (exact words matter): "quantum compute an AI agent can
|
|
152
|
+
spend safely — quote-before-spend enforced, auditable receipts." **Never
|
|
153
|
+
claim "first MCP"** (IBM's Qiskit MCP servers and Conductor's CODA exist);
|
|
154
|
+
"first *governed* one" is the true, stronger claim.
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# quantufai — the QuantufAI Python SDK
|
|
2
|
+
|
|
3
|
+
Quantum compute a program can **price and read — never spend**.
|
|
4
|
+
|
|
5
|
+
> ⚠️ **Publish gate:** this package is repo-code only. It is **not** on PyPI
|
|
6
|
+
> and must not be published or linked from public docs until the founder
|
|
7
|
+
> confirms counsel's provisional filing. Local installs are fine:
|
|
8
|
+
> `pip install -e sdks/python`.
|
|
9
|
+
|
|
10
|
+
## What this SDK can do
|
|
11
|
+
|
|
12
|
+
| You want | Call | Key scope |
|
|
13
|
+
|---|---|---|
|
|
14
|
+
| "What would this run cost?" | `client.quote(circuit, shots=...)` | `quotes:read` |
|
|
15
|
+
| "Is it done yet?" | `client.job_status(job_id)` / `client.wait(job_id)` | `jobs:read` |
|
|
16
|
+
| Counts + ledger + error bars, verbatim | `client.job_result(job_id)` | `jobs:read` |
|
|
17
|
+
| The signed, tamper-evident receipt | `client.governed_receipt(job_id)` | `jobs:read` |
|
|
18
|
+
| Check a receipt you hold | `client.verify_receipt(receipt)` | `jobs:read` (platform tier) / none (offline tier) |
|
|
19
|
+
| The exact circuit that ran, as Qiskit/Cirq/Braket/pytket/QASM | `client.export_circuit(job_id, format=..., which=...)` | `results:export` |
|
|
20
|
+
| Download the result artifact | `client.export_result(job_id)` | `results:export` |
|
|
21
|
+
| Run on the **free local simulator** ($0) | `client.sandbox_simulate(circuit)` | `runs:simulate` (sandbox key) |
|
|
22
|
+
| What can this key do? | `client.me()` | `account:read` |
|
|
23
|
+
|
|
24
|
+
## What this SDK cannot do — by design, not omission
|
|
25
|
+
|
|
26
|
+
**There is no dispatch method. There is no approval method. There is no
|
|
27
|
+
billing method.** Spending money on quantum hardware requires a human
|
|
28
|
+
approving a signed quote in the QuantufAI dashboard:
|
|
29
|
+
|
|
30
|
+
- The platform enforces quote-before-spend server-side; scoped API keys
|
|
31
|
+
cannot approve a spend.
|
|
32
|
+
- On top of that, this SDK's one execution method (`sandbox_simulate`) pins
|
|
33
|
+
every request to the free local simulator (`preferredProviders:
|
|
34
|
+
["classical"]`), so **even a key carrying the paid `runs:execute` scope
|
|
35
|
+
cannot reach billable hardware through this SDK**. Zero eligible providers
|
|
36
|
+
is a typed failure on the platform — never a silent reroute.
|
|
37
|
+
|
|
38
|
+
An AI agent driving this client can tell you exactly what an experiment would
|
|
39
|
+
cost and read every receipt — and cannot buy anything.
|
|
40
|
+
|
|
41
|
+
## REFUSED is a status, not an exception
|
|
42
|
+
|
|
43
|
+
Every deliberate platform refusal — missing scope, sandbox clamp, someone
|
|
44
|
+
else's job, quota exhausted, unprovable circuit translation — comes back as a
|
|
45
|
+
typed `Refusal` with the platform's `code`, `message`, and `details`
|
|
46
|
+
verbatim:
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
result = client.job_result("not-my-job")
|
|
50
|
+
if isinstance(result, quantufai.Refusal):
|
|
51
|
+
print(result.status) # "REFUSED"
|
|
52
|
+
print(result.code) # e.g. "job_not_found", "insufficient_scope"
|
|
53
|
+
print(result.details) # e.g. {"requiredScope": "jobs:read", ...}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Exceptions are reserved for transport failures (`TransportError`) and
|
|
57
|
+
unexpected 5xx answers (`PlatformError`, body preserved).
|
|
58
|
+
|
|
59
|
+
## Quickstart (sandbox — no card, $0)
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
import quantufai
|
|
63
|
+
|
|
64
|
+
client = quantufai.Client(
|
|
65
|
+
api_key="qfai_sk_...", # or $QUANTUFAI_API_KEY; sandbox tier: mint
|
|
66
|
+
# with {"tier": "sandbox"} in the dashboard
|
|
67
|
+
base_url="https://YOUR_HOST", # or $QUANTUFAI_BASE_URL
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
bell = """OPENQASM 2.0;
|
|
71
|
+
include "qelib1.inc";
|
|
72
|
+
qreg q[2]; creg c[2];
|
|
73
|
+
h q[0]; cx q[0], q[1];
|
|
74
|
+
measure q -> c;"""
|
|
75
|
+
|
|
76
|
+
# 1. Price it first — nothing is reserved, charged, or dispatched by quoting.
|
|
77
|
+
quotes = client.quote(bell, shots=1000, qubits=2)
|
|
78
|
+
if isinstance(quotes, quantufai.Refusal):
|
|
79
|
+
raise SystemExit(f"refused: {quotes.code} — {quotes.message}")
|
|
80
|
+
for q in quotes.quotes:
|
|
81
|
+
print(f"{q.provider}: ~${q.estimated_cost_usd} ({q.reasons})")
|
|
82
|
+
|
|
83
|
+
# 2. Run on the FREE local simulator (the only execution this SDK performs).
|
|
84
|
+
run = client.sandbox_simulate(bell, shots=1000, qubits=2)
|
|
85
|
+
if isinstance(run, quantufai.Refusal):
|
|
86
|
+
raise SystemExit(f"refused: {run.code} — {run.message}")
|
|
87
|
+
print(run.state, run.counts) # real statevector physics, real counts, $0
|
|
88
|
+
|
|
89
|
+
# 3. Poll (async runs), then fetch the signed receipt.
|
|
90
|
+
result = client.wait(run.job_id)
|
|
91
|
+
receipt = client.governed_receipt(run.job_id)
|
|
92
|
+
|
|
93
|
+
# 4. Verify the receipt — offline Ed25519 tier and/or the platform's check.
|
|
94
|
+
report = client.verify_receipt(receipt)
|
|
95
|
+
print(report.verified, report.offline.public_signature)
|
|
96
|
+
|
|
97
|
+
# 5. Take the exact circuit home. `which` is required — the platform refuses
|
|
98
|
+
# to guess between "original" and "as-dispatched".
|
|
99
|
+
code = client.export_circuit(run.job_id, format="qiskit", which="as-dispatched")
|
|
100
|
+
print(code.source)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Receipts: what the tiers mean
|
|
104
|
+
|
|
105
|
+
- **Publicly-verifiable tier** (`publicSignature`, Ed25519): anyone can
|
|
106
|
+
verify offline with QuantufAI's published public key —
|
|
107
|
+
`verify_receipt(receipt, public_key=..., offline_only=True)` or the
|
|
108
|
+
standalone `tools/verify-receipt.mjs`. Needs the optional extra:
|
|
109
|
+
`pip install 'quantufai[verify]'`.
|
|
110
|
+
- **Server-attested tier** (`signature`, HMAC): the platform's own
|
|
111
|
+
attestation; only the platform can check it (that's what the platform tier
|
|
112
|
+
of `verify_receipt` asks for). Older receipts carry only this tier — the
|
|
113
|
+
SDK reports that honestly instead of pretending to a verdict.
|
|
114
|
+
|
|
115
|
+
## Results are verbatim
|
|
116
|
+
|
|
117
|
+
Counts are never re-binned, error bars are never computed client-side —
|
|
118
|
+
`JobResult.error_bars` returns exactly the uncertainty fields the platform
|
|
119
|
+
attached (with their location in the payload), and returns nothing when the
|
|
120
|
+
platform attached none.
|
|
121
|
+
|
|
122
|
+
## History: this package replaces deleted fabricating stubs
|
|
123
|
+
|
|
124
|
+
The repo previously carried `sdks/python/quantufai.py` and
|
|
125
|
+
`sdks/js/quantufai.ts` — stubs that called endpoints that never existed and
|
|
126
|
+
**invented job statuses by pattern-matching chat text**. They were deleted
|
|
127
|
+
(PR #579) and a CI test keeps them deleted. This package is their honest
|
|
128
|
+
replacement: every call maps to a real, mounted, scope-gated endpoint, and
|
|
129
|
+
anything the platform refuses surfaces as a typed `REFUSED`.
|
|
130
|
+
|
|
131
|
+
## Publish checklist (post-filing — founder go required)
|
|
132
|
+
|
|
133
|
+
1. Founder confirms counsel's provisional filing covers the receipt/verifier
|
|
134
|
+
disclosures (roadmap #4 gate).
|
|
135
|
+
2. `python -m build` from `sdks/python/`; check the wheel installs clean.
|
|
136
|
+
3. Reserve/publish `quantufai` on PyPI from the org account (never a personal
|
|
137
|
+
one); enable 2FA + trusted publishing.
|
|
138
|
+
4. Only then link the SDK from `/docs` (the docs deliberately do not mention
|
|
139
|
+
it today).
|
|
140
|
+
5. Announcement claim (exact words matter): "quantum compute an AI agent can
|
|
141
|
+
spend safely — quote-before-spend enforced, auditable receipts." **Never
|
|
142
|
+
claim "first MCP"** (IBM's Qiskit MCP servers and Conductor's CODA exist);
|
|
143
|
+
"first *governed* one" is the true, stronger claim.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
##############################################################################
|
|
2
|
+
# quantufai — the official QuantufAI Python SDK
|
|
3
|
+
#
|
|
4
|
+
# ⚠️ PUBLISH GATE (founder/counsel — DO NOT REMOVE): this package is
|
|
5
|
+
# repo-code ONLY. Do NOT publish to PyPI and do NOT link it from public docs
|
|
6
|
+
# until the founder confirms counsel's provisional filing (the same exposure
|
|
7
|
+
# gate as the publicly-verifiable-receipts surface, advancement roadmap #4).
|
|
8
|
+
# Building/installing locally (`pip install -e sdks/python`) is fine.
|
|
9
|
+
##############################################################################
|
|
10
|
+
|
|
11
|
+
[build-system]
|
|
12
|
+
requires = ["setuptools>=68"]
|
|
13
|
+
build-backend = "setuptools.build_meta"
|
|
14
|
+
|
|
15
|
+
[project]
|
|
16
|
+
name = "quantufai"
|
|
17
|
+
version = "0.1.0"
|
|
18
|
+
description = "QuantufAI developer SDK: quote quantum runs before any spend, read job status/results/receipts, export circuits, and simulate on the free sandbox — this SDK can price and read, it can never spend."
|
|
19
|
+
readme = "README.md"
|
|
20
|
+
requires-python = ">=3.9"
|
|
21
|
+
license = { text = "Proprietary" }
|
|
22
|
+
authors = [{ name = "QuantufAI, Inc." }]
|
|
23
|
+
# ZERO runtime dependencies by design: the client is stdlib urllib. The one
|
|
24
|
+
# optional extra is offline receipt verification (Ed25519 needs `cryptography`).
|
|
25
|
+
dependencies = []
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
verify = ["cryptography>=41"]
|
|
29
|
+
|
|
30
|
+
[tool.setuptools]
|
|
31
|
+
packages = ["quantufai"]
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""quantufai — the official QuantufAI Python SDK.
|
|
2
|
+
|
|
3
|
+
Quantum compute a program can PRICE and READ, but never SPEND: quotes before
|
|
4
|
+
any spend, job status/results with receipts and error bars verbatim, signed
|
|
5
|
+
receipt verification, circuit export, and a free sandbox simulator. There is
|
|
6
|
+
no dispatch or approval method — spending stays with a human in the dashboard
|
|
7
|
+
(see ``quantufai.client`` for the full contract).
|
|
8
|
+
|
|
9
|
+
This package intentionally replaces the repo's former SDK stubs, which called
|
|
10
|
+
endpoints that did not exist and fabricated job statuses from pattern-matched
|
|
11
|
+
text. Everything here calls the real, mounted, scope-gated API — and what the
|
|
12
|
+
API refuses, this SDK reports as a typed ``REFUSED``, never a made-up answer.
|
|
13
|
+
"""
|
|
14
|
+
from ._version import __version__
|
|
15
|
+
from .client import FREE_SIMULATOR_PROVIDER, Client, ReceiptVerification
|
|
16
|
+
from .types import (
|
|
17
|
+
FAILED,
|
|
18
|
+
QUEUED,
|
|
19
|
+
REFUSED,
|
|
20
|
+
RUNNING,
|
|
21
|
+
SUCCEEDED,
|
|
22
|
+
CircuitExport,
|
|
23
|
+
GovernedReceipt,
|
|
24
|
+
JobResult,
|
|
25
|
+
JobStatus,
|
|
26
|
+
Me,
|
|
27
|
+
PlatformError,
|
|
28
|
+
Quote,
|
|
29
|
+
QuoteResponse,
|
|
30
|
+
ReceiptBundle,
|
|
31
|
+
Refusal,
|
|
32
|
+
ResultExport,
|
|
33
|
+
SandboxRun,
|
|
34
|
+
TransportError,
|
|
35
|
+
is_terminal,
|
|
36
|
+
normalize_state,
|
|
37
|
+
)
|
|
38
|
+
from .verify import ChainCheck, OfflineVerification, verify_receipt_offline
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
"__version__",
|
|
42
|
+
"Client",
|
|
43
|
+
"FREE_SIMULATOR_PROVIDER",
|
|
44
|
+
"ReceiptVerification",
|
|
45
|
+
"Refusal",
|
|
46
|
+
"REFUSED",
|
|
47
|
+
"QUEUED",
|
|
48
|
+
"RUNNING",
|
|
49
|
+
"SUCCEEDED",
|
|
50
|
+
"FAILED",
|
|
51
|
+
"Quote",
|
|
52
|
+
"QuoteResponse",
|
|
53
|
+
"JobStatus",
|
|
54
|
+
"JobResult",
|
|
55
|
+
"ReceiptBundle",
|
|
56
|
+
"GovernedReceipt",
|
|
57
|
+
"CircuitExport",
|
|
58
|
+
"ResultExport",
|
|
59
|
+
"SandboxRun",
|
|
60
|
+
"Me",
|
|
61
|
+
"TransportError",
|
|
62
|
+
"PlatformError",
|
|
63
|
+
"normalize_state",
|
|
64
|
+
"is_terminal",
|
|
65
|
+
"OfflineVerification",
|
|
66
|
+
"ChainCheck",
|
|
67
|
+
"verify_receipt_offline",
|
|
68
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Canonical JSON — a byte-exact Python port of the platform's stableStringify.
|
|
2
|
+
|
|
3
|
+
Receipt signatures cover ``stableStringify(receipt minus signature fields)``:
|
|
4
|
+
deterministic JSON with lexicographically sorted object keys, serialized the
|
|
5
|
+
way JavaScript's ``JSON.stringify`` serializes scalars. Verification in Python
|
|
6
|
+
therefore needs the SAME bytes — including JavaScript's number formatting
|
|
7
|
+
(``1e21`` -> ``"1e+21"``, ``1e-7`` -> ``"1e-7"``, ``1.0`` -> ``"1"``), which
|
|
8
|
+
differs from Python's ``json.dumps`` in the corners.
|
|
9
|
+
|
|
10
|
+
Parity is pinned by shared test vectors (tests/fixtures/canonical_vectors.json)
|
|
11
|
+
asserted against both this module and the Node implementation.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import math
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _js_number(value: float) -> str:
|
|
21
|
+
"""Format a float exactly as ECMAScript Number::toString(10) would.
|
|
22
|
+
|
|
23
|
+
JSON.stringify emits non-finite numbers as ``null``; integral values
|
|
24
|
+
without a fractional part; exponential notation only for |exponent|
|
|
25
|
+
outside [-6, 21); exponents unpadded with an explicit sign.
|
|
26
|
+
"""
|
|
27
|
+
if math.isnan(value) or math.isinf(value):
|
|
28
|
+
return "null" # JSON.stringify(NaN) === "null"
|
|
29
|
+
if value == 0:
|
|
30
|
+
return "0"
|
|
31
|
+
# Shortest round-trip decimal digits (repr is shortest in Python >= 3.1,
|
|
32
|
+
# same guarantee V8 uses).
|
|
33
|
+
rep = repr(abs(value))
|
|
34
|
+
if "e" in rep or "E" in rep:
|
|
35
|
+
mantissa, _, exp_part = rep.lower().partition("e")
|
|
36
|
+
exp10 = int(exp_part)
|
|
37
|
+
else:
|
|
38
|
+
mantissa, exp10 = rep, 0
|
|
39
|
+
int_part, _, frac_part = mantissa.partition(".")
|
|
40
|
+
digits = (int_part + frac_part).lstrip("0")
|
|
41
|
+
# Decimal point position: value = 0.<digits> * 10**point
|
|
42
|
+
point = len(int_part.lstrip("0")) + exp10 if int_part.strip("0") else (
|
|
43
|
+
exp10 - (len(frac_part) - len(frac_part.lstrip("0")))
|
|
44
|
+
)
|
|
45
|
+
digits = digits.rstrip("0") or "0"
|
|
46
|
+
sign = "-" if value < 0 else ""
|
|
47
|
+
|
|
48
|
+
k = len(digits)
|
|
49
|
+
if k <= point <= 21: # integral, printed in full
|
|
50
|
+
return sign + digits + "0" * (point - k)
|
|
51
|
+
if 0 < point <= 21: # decimal point inside the digits
|
|
52
|
+
return sign + digits[:point] + "." + digits[point:]
|
|
53
|
+
if -6 < point <= 0: # small: leading zeros after "0."
|
|
54
|
+
return sign + "0." + "0" * (-point) + digits
|
|
55
|
+
# Exponential: d[.ddd]e±e (exponent = point - 1, unpadded, signed)
|
|
56
|
+
exponent = point - 1
|
|
57
|
+
exp_str = ("+" if exponent >= 0 else "-") + str(abs(exponent))
|
|
58
|
+
if k == 1:
|
|
59
|
+
return sign + digits + "e" + exp_str
|
|
60
|
+
return sign + digits[0] + "." + digits[1:] + "e" + exp_str
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def stable_stringify(value: Any) -> str:
|
|
64
|
+
"""Deterministic JSON with sorted object keys — parity with the platform."""
|
|
65
|
+
if value is None:
|
|
66
|
+
return "null"
|
|
67
|
+
if value is True:
|
|
68
|
+
return "true"
|
|
69
|
+
if value is False:
|
|
70
|
+
return "false"
|
|
71
|
+
if isinstance(value, str):
|
|
72
|
+
# ensure_ascii=False matches JSON.stringify (raw UTF-8, control chars
|
|
73
|
+
# escaped); Python and JS agree on the mandatory escape set.
|
|
74
|
+
return json.dumps(value, ensure_ascii=False)
|
|
75
|
+
if isinstance(value, int):
|
|
76
|
+
return str(value)
|
|
77
|
+
if isinstance(value, float):
|
|
78
|
+
# JS has one number type: 1.0 serializes as "1" (handled in _js_number).
|
|
79
|
+
return _js_number(value)
|
|
80
|
+
if isinstance(value, (list, tuple)):
|
|
81
|
+
return "[" + ",".join(stable_stringify(v) for v in value) + "]"
|
|
82
|
+
if isinstance(value, dict):
|
|
83
|
+
# JavaScript's Array.prototype.sort() compares strings by UTF-16 code
|
|
84
|
+
# units; utf-16-be byte order reproduces that exactly (receipt keys are
|
|
85
|
+
# ASCII in practice, but parity should not depend on practice).
|
|
86
|
+
items = sorted(
|
|
87
|
+
((str(k), v) for k, v in value.items()),
|
|
88
|
+
key=lambda kv: kv[0].encode("utf-16-be"),
|
|
89
|
+
)
|
|
90
|
+
return "{" + ",".join(
|
|
91
|
+
json.dumps(k, ensure_ascii=False) + ":" + stable_stringify(v) for k, v in items
|
|
92
|
+
) + "}"
|
|
93
|
+
raise TypeError(f"stable_stringify: unsupported type {type(value).__name__}")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def unsigned_canonical(receipt: dict) -> str:
|
|
97
|
+
"""The exact bytes both platform signatures cover: the receipt minus its
|
|
98
|
+
``signature`` and ``publicSignature`` fields."""
|
|
99
|
+
clone = {k: v for k, v in receipt.items() if k not in ("signature", "publicSignature")}
|
|
100
|
+
return stable_stringify(clone)
|