orangecheck 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.
- orangecheck-0.1.0/.gitignore +38 -0
- orangecheck-0.1.0/PKG-INFO +261 -0
- orangecheck-0.1.0/README.md +228 -0
- orangecheck-0.1.0/orangecheck/__init__.py +60 -0
- orangecheck-0.1.0/orangecheck/__main__.py +68 -0
- orangecheck-0.1.0/orangecheck/client.py +413 -0
- orangecheck-0.1.0/orangecheck/errors.py +20 -0
- orangecheck-0.1.0/orangecheck/py.typed +0 -0
- orangecheck-0.1.0/orangecheck/top_level.py +101 -0
- orangecheck-0.1.0/orangecheck/types.py +169 -0
- orangecheck-0.1.0/pyproject.toml +71 -0
- orangecheck-0.1.0/tests/test_client.py +202 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Dependencies
|
|
2
|
+
node_modules
|
|
3
|
+
**/node_modules
|
|
4
|
+
|
|
5
|
+
# Build outputs
|
|
6
|
+
dist
|
|
7
|
+
**/dist
|
|
8
|
+
*.tsbuildinfo
|
|
9
|
+
|
|
10
|
+
# Python
|
|
11
|
+
__pycache__
|
|
12
|
+
*.pyc
|
|
13
|
+
.pytest_cache
|
|
14
|
+
.venv
|
|
15
|
+
**/*.egg-info
|
|
16
|
+
|
|
17
|
+
# Test + coverage
|
|
18
|
+
coverage
|
|
19
|
+
.nyc_output
|
|
20
|
+
|
|
21
|
+
# Tool caches
|
|
22
|
+
.turbo
|
|
23
|
+
.cache
|
|
24
|
+
|
|
25
|
+
# OS
|
|
26
|
+
.DS_Store
|
|
27
|
+
*.swp
|
|
28
|
+
|
|
29
|
+
# Secrets
|
|
30
|
+
.env
|
|
31
|
+
.env.local
|
|
32
|
+
.env.*.local
|
|
33
|
+
|
|
34
|
+
# Logs
|
|
35
|
+
*.log
|
|
36
|
+
npm-debug.log*
|
|
37
|
+
yarn-debug.log*
|
|
38
|
+
yarn-error.log*
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: orangecheck
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for OrangeCheck — proof of Bitcoin stake for the open web.
|
|
5
|
+
Project-URL: Homepage, https://ochk.io
|
|
6
|
+
Project-URL: Documentation, https://ochk.io/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/orangecheck/oc-web
|
|
8
|
+
Project-URL: Issues, https://github.com/orangecheck/oc-web/issues
|
|
9
|
+
Author: OrangeCheck
|
|
10
|
+
License: MIT
|
|
11
|
+
Keywords: attestation,bip322,bitcoin,nostr,orangecheck,sybil-resistance
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
22
|
+
Classifier: Topic :: Security
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.9
|
|
25
|
+
Requires-Dist: httpx>=0.25
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: mypy>=1.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: respx>=0.20; extra == 'dev'
|
|
31
|
+
Requires-Dist: ruff>=0.1; extra == 'dev'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# `orangecheck` — Python SDK
|
|
35
|
+
|
|
36
|
+
**Proof of Bitcoin stake for the open web.** Python SDK for OrangeCheck.
|
|
37
|
+
|
|
38
|
+
A sybil-resistance primitive any Python app can consume in one call. Works with Django, Flask, FastAPI, Starlette, or a plain script.
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install orangecheck
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## The 30-second integration
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from orangecheck import check
|
|
50
|
+
|
|
51
|
+
result = check(addr="bc1q...", min_sats=100_000, min_days=30)
|
|
52
|
+
|
|
53
|
+
if result.ok:
|
|
54
|
+
# let them through
|
|
55
|
+
...
|
|
56
|
+
else:
|
|
57
|
+
print("rejected:", result.reasons)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
That's it. `check()` queries the hosted `ochk.io` API, which discovers the most recent attestation for the address on Nostr, verifies the Bitcoin signature, recomputes live chain state, and compares against your thresholds.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Load-bearing functions
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from orangecheck import check, verify, discover, challenge_issue, challenge_verify
|
|
68
|
+
|
|
69
|
+
# Gate
|
|
70
|
+
check(addr="bc1q...", min_sats=100_000, min_days=30)
|
|
71
|
+
check(id="a3f5b8c2...", min_sats=100_000)
|
|
72
|
+
check(identity="github:alice", min_sats=50_000)
|
|
73
|
+
|
|
74
|
+
# Verify a raw attestation
|
|
75
|
+
verify(addr="bc1q...", msg=canonical_message, sig=signature)
|
|
76
|
+
|
|
77
|
+
# List attestations for a subject
|
|
78
|
+
discover(addr="bc1q...", limit=10)
|
|
79
|
+
|
|
80
|
+
# Signed-challenge auth (prove address control)
|
|
81
|
+
ch = challenge_issue(addr="bc1q...", audience="https://example.com")
|
|
82
|
+
verified = challenge_verify(message=ch.message, signature=user_sig, expected_nonce=ch.nonce)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
All return typed dataclasses. All raise `OrangeCheckError` (or subclasses) on transport / server errors.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Django integration
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
# views.py
|
|
93
|
+
from django.http import HttpResponse, JsonResponse
|
|
94
|
+
from orangecheck import check
|
|
95
|
+
|
|
96
|
+
def gated_post(request):
|
|
97
|
+
addr = request.session.get("btc_address")
|
|
98
|
+
if not addr:
|
|
99
|
+
return HttpResponse(status=401)
|
|
100
|
+
result = check(addr=addr, min_sats=100_000, min_days=30)
|
|
101
|
+
if not result.ok:
|
|
102
|
+
return JsonResponse({"error": "orangecheck", "reasons": result.reasons}, status=403)
|
|
103
|
+
# ... proceed
|
|
104
|
+
return JsonResponse({"ok": True})
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### As middleware
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
# orangecheck_middleware.py
|
|
111
|
+
from django.http import JsonResponse
|
|
112
|
+
from orangecheck import check
|
|
113
|
+
|
|
114
|
+
class OrangeCheckMiddleware:
|
|
115
|
+
def __init__(self, get_response):
|
|
116
|
+
self.get_response = get_response
|
|
117
|
+
|
|
118
|
+
def __call__(self, request):
|
|
119
|
+
if request.path.startswith("/protected/"):
|
|
120
|
+
addr = request.session.get("btc_address")
|
|
121
|
+
r = check(addr=addr, min_sats=100_000, min_days=30)
|
|
122
|
+
if not r.ok:
|
|
123
|
+
return JsonResponse({"error": r.reasons}, status=403)
|
|
124
|
+
return self.get_response(request)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## FastAPI integration
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
from fastapi import FastAPI, Depends, HTTPException
|
|
133
|
+
from orangecheck import AsyncClient
|
|
134
|
+
|
|
135
|
+
app = FastAPI()
|
|
136
|
+
oc = AsyncClient()
|
|
137
|
+
|
|
138
|
+
async def require_stake(addr: str, min_sats: int = 100_000, min_days: int = 30):
|
|
139
|
+
r = await oc.check(addr=addr, min_sats=min_sats, min_days=min_days)
|
|
140
|
+
if not r.ok:
|
|
141
|
+
raise HTTPException(status_code=403, detail={"reasons": list(r.reasons)})
|
|
142
|
+
return r
|
|
143
|
+
|
|
144
|
+
@app.post("/post")
|
|
145
|
+
async def post_comment(gate = Depends(require_stake)):
|
|
146
|
+
return {"ok": True, "sats": gate.sats}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Flask integration
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
from functools import wraps
|
|
155
|
+
from flask import Flask, request, jsonify
|
|
156
|
+
from orangecheck import check
|
|
157
|
+
|
|
158
|
+
app = Flask(__name__)
|
|
159
|
+
|
|
160
|
+
def require_orangecheck(min_sats=100_000, min_days=30):
|
|
161
|
+
def decorator(fn):
|
|
162
|
+
@wraps(fn)
|
|
163
|
+
def wrapper(*args, **kwargs):
|
|
164
|
+
addr = request.headers.get("X-OC-Address")
|
|
165
|
+
r = check(addr=addr, min_sats=min_sats, min_days=min_days)
|
|
166
|
+
if not r.ok:
|
|
167
|
+
return jsonify(error="orangecheck", reasons=r.reasons), 403
|
|
168
|
+
return fn(*args, **kwargs)
|
|
169
|
+
return wrapper
|
|
170
|
+
return decorator
|
|
171
|
+
|
|
172
|
+
@app.post("/post")
|
|
173
|
+
@require_orangecheck(min_sats=100_000, min_days=30)
|
|
174
|
+
def post_comment():
|
|
175
|
+
return {"ok": True}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Async
|
|
181
|
+
|
|
182
|
+
Every function has a sync counterpart on `AsyncClient`:
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
import asyncio
|
|
186
|
+
from orangecheck import AsyncClient
|
|
187
|
+
|
|
188
|
+
async def main():
|
|
189
|
+
async with AsyncClient() as oc:
|
|
190
|
+
r = await oc.check(addr="bc1q...", min_sats=100_000)
|
|
191
|
+
print(r.ok, r.sats, r.days)
|
|
192
|
+
|
|
193
|
+
asyncio.run(main())
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## Configuration
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
from orangecheck import Client
|
|
202
|
+
|
|
203
|
+
# Default — hits https://ochk.io
|
|
204
|
+
c = Client()
|
|
205
|
+
|
|
206
|
+
# Self-hosted verifier, custom timeout
|
|
207
|
+
c = Client(base_url="https://verifier.mycompany.com", timeout=5.0)
|
|
208
|
+
|
|
209
|
+
# Reuse an existing httpx Client (connection pooling, auth, etc.)
|
|
210
|
+
import httpx
|
|
211
|
+
my_session = httpx.Client(proxies="http://proxy.example.com")
|
|
212
|
+
c = Client(session=my_session)
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## Types
|
|
218
|
+
|
|
219
|
+
Every response is a frozen `dataclass` with predictable fields:
|
|
220
|
+
|
|
221
|
+
```python
|
|
222
|
+
@dataclass(frozen=True)
|
|
223
|
+
class CheckResult:
|
|
224
|
+
ok: bool
|
|
225
|
+
sats: int
|
|
226
|
+
days: int
|
|
227
|
+
score: float
|
|
228
|
+
attestation_id: str | None
|
|
229
|
+
address: str | None
|
|
230
|
+
identities: tuple[IdentityBinding, ...]
|
|
231
|
+
network: Literal["mainnet", "testnet", "signet"] | None
|
|
232
|
+
reasons: tuple[str, ...]
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Full type information ships with the package (`py.typed` marker, tested with mypy strict).
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## Errors
|
|
240
|
+
|
|
241
|
+
- **`OrangeCheckError`** — base class for everything else.
|
|
242
|
+
- **`RateLimitError`** — the hosted API returned 429.
|
|
243
|
+
- **`VerificationError`** — `challenge_verify` failed (bad signature, expired, nonce mismatch, …).
|
|
244
|
+
|
|
245
|
+
`check()` treats 404 (no attestation found) as `CheckResult(ok=False, reasons=("not_found",))` rather than raising — callers should gate on `.ok`, not on try/except.
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## Shell smoke-test
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
python -m orangecheck check --addr bc1q... --min-sats 100000
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Exits `0` on pass, `2` on fail. Prefer the TypeScript `oc` CLI for richer output; this one is here mostly to verify the install.
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## License
|
|
260
|
+
|
|
261
|
+
MIT. The OrangeCheck protocol is CC-BY-4.0.
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# `orangecheck` — Python SDK
|
|
2
|
+
|
|
3
|
+
**Proof of Bitcoin stake for the open web.** Python SDK for OrangeCheck.
|
|
4
|
+
|
|
5
|
+
A sybil-resistance primitive any Python app can consume in one call. Works with Django, Flask, FastAPI, Starlette, or a plain script.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install orangecheck
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## The 30-second integration
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from orangecheck import check
|
|
17
|
+
|
|
18
|
+
result = check(addr="bc1q...", min_sats=100_000, min_days=30)
|
|
19
|
+
|
|
20
|
+
if result.ok:
|
|
21
|
+
# let them through
|
|
22
|
+
...
|
|
23
|
+
else:
|
|
24
|
+
print("rejected:", result.reasons)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
That's it. `check()` queries the hosted `ochk.io` API, which discovers the most recent attestation for the address on Nostr, verifies the Bitcoin signature, recomputes live chain state, and compares against your thresholds.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Load-bearing functions
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from orangecheck import check, verify, discover, challenge_issue, challenge_verify
|
|
35
|
+
|
|
36
|
+
# Gate
|
|
37
|
+
check(addr="bc1q...", min_sats=100_000, min_days=30)
|
|
38
|
+
check(id="a3f5b8c2...", min_sats=100_000)
|
|
39
|
+
check(identity="github:alice", min_sats=50_000)
|
|
40
|
+
|
|
41
|
+
# Verify a raw attestation
|
|
42
|
+
verify(addr="bc1q...", msg=canonical_message, sig=signature)
|
|
43
|
+
|
|
44
|
+
# List attestations for a subject
|
|
45
|
+
discover(addr="bc1q...", limit=10)
|
|
46
|
+
|
|
47
|
+
# Signed-challenge auth (prove address control)
|
|
48
|
+
ch = challenge_issue(addr="bc1q...", audience="https://example.com")
|
|
49
|
+
verified = challenge_verify(message=ch.message, signature=user_sig, expected_nonce=ch.nonce)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
All return typed dataclasses. All raise `OrangeCheckError` (or subclasses) on transport / server errors.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Django integration
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
# views.py
|
|
60
|
+
from django.http import HttpResponse, JsonResponse
|
|
61
|
+
from orangecheck import check
|
|
62
|
+
|
|
63
|
+
def gated_post(request):
|
|
64
|
+
addr = request.session.get("btc_address")
|
|
65
|
+
if not addr:
|
|
66
|
+
return HttpResponse(status=401)
|
|
67
|
+
result = check(addr=addr, min_sats=100_000, min_days=30)
|
|
68
|
+
if not result.ok:
|
|
69
|
+
return JsonResponse({"error": "orangecheck", "reasons": result.reasons}, status=403)
|
|
70
|
+
# ... proceed
|
|
71
|
+
return JsonResponse({"ok": True})
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### As middleware
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
# orangecheck_middleware.py
|
|
78
|
+
from django.http import JsonResponse
|
|
79
|
+
from orangecheck import check
|
|
80
|
+
|
|
81
|
+
class OrangeCheckMiddleware:
|
|
82
|
+
def __init__(self, get_response):
|
|
83
|
+
self.get_response = get_response
|
|
84
|
+
|
|
85
|
+
def __call__(self, request):
|
|
86
|
+
if request.path.startswith("/protected/"):
|
|
87
|
+
addr = request.session.get("btc_address")
|
|
88
|
+
r = check(addr=addr, min_sats=100_000, min_days=30)
|
|
89
|
+
if not r.ok:
|
|
90
|
+
return JsonResponse({"error": r.reasons}, status=403)
|
|
91
|
+
return self.get_response(request)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## FastAPI integration
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from fastapi import FastAPI, Depends, HTTPException
|
|
100
|
+
from orangecheck import AsyncClient
|
|
101
|
+
|
|
102
|
+
app = FastAPI()
|
|
103
|
+
oc = AsyncClient()
|
|
104
|
+
|
|
105
|
+
async def require_stake(addr: str, min_sats: int = 100_000, min_days: int = 30):
|
|
106
|
+
r = await oc.check(addr=addr, min_sats=min_sats, min_days=min_days)
|
|
107
|
+
if not r.ok:
|
|
108
|
+
raise HTTPException(status_code=403, detail={"reasons": list(r.reasons)})
|
|
109
|
+
return r
|
|
110
|
+
|
|
111
|
+
@app.post("/post")
|
|
112
|
+
async def post_comment(gate = Depends(require_stake)):
|
|
113
|
+
return {"ok": True, "sats": gate.sats}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Flask integration
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
from functools import wraps
|
|
122
|
+
from flask import Flask, request, jsonify
|
|
123
|
+
from orangecheck import check
|
|
124
|
+
|
|
125
|
+
app = Flask(__name__)
|
|
126
|
+
|
|
127
|
+
def require_orangecheck(min_sats=100_000, min_days=30):
|
|
128
|
+
def decorator(fn):
|
|
129
|
+
@wraps(fn)
|
|
130
|
+
def wrapper(*args, **kwargs):
|
|
131
|
+
addr = request.headers.get("X-OC-Address")
|
|
132
|
+
r = check(addr=addr, min_sats=min_sats, min_days=min_days)
|
|
133
|
+
if not r.ok:
|
|
134
|
+
return jsonify(error="orangecheck", reasons=r.reasons), 403
|
|
135
|
+
return fn(*args, **kwargs)
|
|
136
|
+
return wrapper
|
|
137
|
+
return decorator
|
|
138
|
+
|
|
139
|
+
@app.post("/post")
|
|
140
|
+
@require_orangecheck(min_sats=100_000, min_days=30)
|
|
141
|
+
def post_comment():
|
|
142
|
+
return {"ok": True}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Async
|
|
148
|
+
|
|
149
|
+
Every function has a sync counterpart on `AsyncClient`:
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
import asyncio
|
|
153
|
+
from orangecheck import AsyncClient
|
|
154
|
+
|
|
155
|
+
async def main():
|
|
156
|
+
async with AsyncClient() as oc:
|
|
157
|
+
r = await oc.check(addr="bc1q...", min_sats=100_000)
|
|
158
|
+
print(r.ok, r.sats, r.days)
|
|
159
|
+
|
|
160
|
+
asyncio.run(main())
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Configuration
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
from orangecheck import Client
|
|
169
|
+
|
|
170
|
+
# Default — hits https://ochk.io
|
|
171
|
+
c = Client()
|
|
172
|
+
|
|
173
|
+
# Self-hosted verifier, custom timeout
|
|
174
|
+
c = Client(base_url="https://verifier.mycompany.com", timeout=5.0)
|
|
175
|
+
|
|
176
|
+
# Reuse an existing httpx Client (connection pooling, auth, etc.)
|
|
177
|
+
import httpx
|
|
178
|
+
my_session = httpx.Client(proxies="http://proxy.example.com")
|
|
179
|
+
c = Client(session=my_session)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Types
|
|
185
|
+
|
|
186
|
+
Every response is a frozen `dataclass` with predictable fields:
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
@dataclass(frozen=True)
|
|
190
|
+
class CheckResult:
|
|
191
|
+
ok: bool
|
|
192
|
+
sats: int
|
|
193
|
+
days: int
|
|
194
|
+
score: float
|
|
195
|
+
attestation_id: str | None
|
|
196
|
+
address: str | None
|
|
197
|
+
identities: tuple[IdentityBinding, ...]
|
|
198
|
+
network: Literal["mainnet", "testnet", "signet"] | None
|
|
199
|
+
reasons: tuple[str, ...]
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Full type information ships with the package (`py.typed` marker, tested with mypy strict).
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## Errors
|
|
207
|
+
|
|
208
|
+
- **`OrangeCheckError`** — base class for everything else.
|
|
209
|
+
- **`RateLimitError`** — the hosted API returned 429.
|
|
210
|
+
- **`VerificationError`** — `challenge_verify` failed (bad signature, expired, nonce mismatch, …).
|
|
211
|
+
|
|
212
|
+
`check()` treats 404 (no attestation found) as `CheckResult(ok=False, reasons=("not_found",))` rather than raising — callers should gate on `.ok`, not on try/except.
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## Shell smoke-test
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
python -m orangecheck check --addr bc1q... --min-sats 100000
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Exits `0` on pass, `2` on fail. Prefer the TypeScript `oc` CLI for richer output; this one is here mostly to verify the install.
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## License
|
|
227
|
+
|
|
228
|
+
MIT. The OrangeCheck protocol is CC-BY-4.0.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""
|
|
2
|
+
orangecheck — Python SDK for OrangeCheck.
|
|
3
|
+
|
|
4
|
+
Proof of Bitcoin stake for the open web. A sybil-resistance primitive for
|
|
5
|
+
Django, Flask, FastAPI, or anything on the Python side of the web.
|
|
6
|
+
|
|
7
|
+
Three load-bearing functions, mirroring the JS SDK:
|
|
8
|
+
|
|
9
|
+
from orangecheck import check, verify, discover
|
|
10
|
+
|
|
11
|
+
result = check(addr="bc1q...", min_sats=100_000, min_days=30)
|
|
12
|
+
if result.ok:
|
|
13
|
+
... # let them through
|
|
14
|
+
|
|
15
|
+
All functions are also available on the Client class for configuring a
|
|
16
|
+
custom API base URL, timeouts, or a shared httpx session:
|
|
17
|
+
|
|
18
|
+
from orangecheck import Client
|
|
19
|
+
oc = Client(base_url="https://my-orangecheck.example.com")
|
|
20
|
+
await oc.check_async(addr="bc1q...", min_sats=100_000)
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from .client import AsyncClient, Client
|
|
24
|
+
from .errors import OrangeCheckError, RateLimitError, VerificationError
|
|
25
|
+
from .top_level import challenge_issue, challenge_verify, check, discover, verify
|
|
26
|
+
from .types import (
|
|
27
|
+
AttestationSummary,
|
|
28
|
+
Challenge,
|
|
29
|
+
ChallengeVerified,
|
|
30
|
+
CheckResult,
|
|
31
|
+
DiscoverResult,
|
|
32
|
+
IdentityBinding,
|
|
33
|
+
VerifyOutcome,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
# Top-level functions (sync)
|
|
38
|
+
"check",
|
|
39
|
+
"verify",
|
|
40
|
+
"discover",
|
|
41
|
+
"challenge_issue",
|
|
42
|
+
"challenge_verify",
|
|
43
|
+
# Clients
|
|
44
|
+
"Client",
|
|
45
|
+
"AsyncClient",
|
|
46
|
+
# Types
|
|
47
|
+
"CheckResult",
|
|
48
|
+
"VerifyOutcome",
|
|
49
|
+
"DiscoverResult",
|
|
50
|
+
"Challenge",
|
|
51
|
+
"ChallengeVerified",
|
|
52
|
+
"IdentityBinding",
|
|
53
|
+
"AttestationSummary",
|
|
54
|
+
# Errors
|
|
55
|
+
"OrangeCheckError",
|
|
56
|
+
"RateLimitError",
|
|
57
|
+
"VerificationError",
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""``python -m orangecheck`` — a tiny CLI, mostly for quick sanity checks.
|
|
2
|
+
|
|
3
|
+
Prefer the TypeScript ``oc`` CLI (``npm install -g @orangecheck/cli``) for
|
|
4
|
+
day-to-day use — it has more features. This one is here so Python users
|
|
5
|
+
can smoke-test the package after ``pip install``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import json
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
from . import __version__, check
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def main() -> int:
|
|
18
|
+
parser = argparse.ArgumentParser(prog="orangecheck", description="OrangeCheck Python SDK")
|
|
19
|
+
parser.add_argument("--version", action="version", version=f"orangecheck {__version__}")
|
|
20
|
+
|
|
21
|
+
sub = parser.add_subparsers(dest="cmd")
|
|
22
|
+
|
|
23
|
+
c = sub.add_parser("check", help="Gate on an OrangeCheck proof")
|
|
24
|
+
c.add_argument("--addr")
|
|
25
|
+
c.add_argument("--id")
|
|
26
|
+
c.add_argument("--identity")
|
|
27
|
+
c.add_argument("--min-sats", type=int, default=0)
|
|
28
|
+
c.add_argument("--min-days", type=int, default=0)
|
|
29
|
+
c.add_argument("--base-url", default=None)
|
|
30
|
+
|
|
31
|
+
args = parser.parse_args()
|
|
32
|
+
|
|
33
|
+
if args.cmd == "check":
|
|
34
|
+
result = check(
|
|
35
|
+
addr=args.addr,
|
|
36
|
+
id=args.id,
|
|
37
|
+
identity=args.identity,
|
|
38
|
+
min_sats=args.min_sats,
|
|
39
|
+
min_days=args.min_days,
|
|
40
|
+
base_url=args.base_url,
|
|
41
|
+
)
|
|
42
|
+
json.dump(
|
|
43
|
+
{
|
|
44
|
+
"ok": result.ok,
|
|
45
|
+
"sats": result.sats,
|
|
46
|
+
"days": result.days,
|
|
47
|
+
"score": result.score,
|
|
48
|
+
"attestation_id": result.attestation_id,
|
|
49
|
+
"address": result.address,
|
|
50
|
+
"identities": [
|
|
51
|
+
{"protocol": i.protocol, "identifier": i.identifier}
|
|
52
|
+
for i in result.identities
|
|
53
|
+
],
|
|
54
|
+
"network": result.network,
|
|
55
|
+
"reasons": list(result.reasons),
|
|
56
|
+
},
|
|
57
|
+
sys.stdout,
|
|
58
|
+
indent=2,
|
|
59
|
+
)
|
|
60
|
+
sys.stdout.write("\n")
|
|
61
|
+
return 0 if result.ok else 2
|
|
62
|
+
|
|
63
|
+
parser.print_help()
|
|
64
|
+
return 1
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
if __name__ == "__main__":
|
|
68
|
+
raise SystemExit(main())
|