Flask decorator
Flask doesn't have DI; a decorator is the cleanest pattern.
Install
pip install flask orangecheck
Minimum working example
from functools import wraps
from flask import Flask, request, jsonify, session
from orangecheck import check
app = Flask(__name__)
app.secret_key = "change-me"
def require_orangecheck(min_sats: int = 100_000, min_days: int = 30):
def decorator(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
addr = request.headers.get("X-OC-Address")
if not addr:
return jsonify(error="missing X-OC-Address"), 400
r = check(addr=addr, min_sats=min_sats, min_days=min_days)
if not r.ok:
return jsonify(error="orangecheck", reasons=list(r.reasons)), 403
request.orangecheck = r
return fn(*args, **kwargs)
return wrapper
return decorator
@app.post("/post")
@require_orangecheck(min_sats=100_000, min_days=30)
def post_comment():
return jsonify(ok=True, sats=request.orangecheck.sats)
Read subject from session
For high-stakes gates, store the proven address in a session cookie (via the signed-challenge flow):
def require_orangecheck(min_sats: int = 100_000, min_days: int = 30):
def decorator(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
addr = session.get("verified_btc_address")
if not addr:
return jsonify(error="not signed in"), 401
r = check(addr=addr, min_sats=min_sats, min_days=min_days)
if not r.ok:
return jsonify(error="orangecheck", reasons=list(r.reasons)), 403
request.orangecheck = r
return fn(*args, **kwargs)
return wrapper
return decorator
Signed-challenge routes
from orangecheck import challenge_issue, challenge_verify, VerificationError
@app.get("/auth/challenge")
def auth_challenge():
addr = request.args.get("addr")
c = challenge_issue(addr=addr, audience="https://example.com", purpose="login")
session["oc_nonce"] = c.nonce
return jsonify(message=c.message)
@app.post("/auth/verify")
def auth_verify():
body = request.get_json()
try:
r = challenge_verify(
message=body["message"],
signature=body["signature"],
expected_nonce=session.get("oc_nonce"),
expected_audience="https://example.com",
)
except VerificationError as e:
return jsonify(error=str(e)), 401
session["verified_btc_address"] = r.address
return jsonify(ok=True, address=r.address)
Multiple thresholds per app
Factor the thresholds out:
POST_GATE = require_orangecheck(min_sats=100_000, min_days=30)
MINT_GATE = require_orangecheck(min_sats=1_000_000, min_days=180)
@app.post("/post") @POST_GATE
def post(): return jsonify(ok=True)
@app.post("/mint") @MINT_GATE
def mint(): return jsonify(ok=True)