docs / flask decorator

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)

Further