uvd-x402-sdk 0.2.0__py3-none-any.whl

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.
@@ -0,0 +1,330 @@
1
+ """
2
+ FastAPI/Starlette integration for x402 payments.
3
+
4
+ Provides:
5
+ - FastAPIX402: App integration class
6
+ - X402Depends: Dependency injection for payment verification
7
+ - fastapi_require_payment: Decorator for protected routes
8
+ """
9
+
10
+ from decimal import Decimal
11
+ from functools import wraps
12
+ from typing import Any, Callable, Optional, TypeVar, Union
13
+
14
+ try:
15
+ from fastapi import FastAPI, Request, HTTPException, Depends
16
+ from fastapi.responses import JSONResponse
17
+ from starlette.middleware.base import BaseHTTPMiddleware
18
+ except ImportError:
19
+ raise ImportError(
20
+ "FastAPI is required for FastAPI integration. "
21
+ "Install with: pip install uvd-x402-sdk[fastapi]"
22
+ )
23
+
24
+ from uvd_x402_sdk.client import X402Client
25
+ from uvd_x402_sdk.config import X402Config
26
+ from uvd_x402_sdk.exceptions import X402Error
27
+ from uvd_x402_sdk.models import PaymentResult
28
+ from uvd_x402_sdk.response import create_402_response, create_402_headers
29
+
30
+ F = TypeVar("F", bound=Callable[..., Any])
31
+
32
+
33
+ class FastAPIX402:
34
+ """
35
+ FastAPI integration for x402 payments.
36
+
37
+ Example:
38
+ >>> from fastapi import FastAPI
39
+ >>> from uvd_x402_sdk.integrations import FastAPIX402
40
+ >>>
41
+ >>> app = FastAPI()
42
+ >>> x402 = FastAPIX402(app, recipient_address="0xYourWallet...")
43
+ >>>
44
+ >>> @app.get("/premium")
45
+ >>> async def premium(payment: PaymentResult = Depends(x402.require_payment(1.00))):
46
+ ... return {"payer": payment.payer_address}
47
+ """
48
+
49
+ def __init__(
50
+ self,
51
+ app: Optional[FastAPI] = None,
52
+ config: Optional[X402Config] = None,
53
+ recipient_address: Optional[str] = None,
54
+ **kwargs: Any,
55
+ ) -> None:
56
+ """
57
+ Initialize FastAPI x402 integration.
58
+
59
+ Args:
60
+ app: FastAPI application (optional)
61
+ config: X402Config object
62
+ recipient_address: Default recipient for EVM chains
63
+ **kwargs: Additional config parameters
64
+ """
65
+ self._config = config or X402Config(
66
+ recipient_evm=recipient_address or "",
67
+ **kwargs,
68
+ )
69
+ self._client = X402Client(config=self._config)
70
+
71
+ if app is not None:
72
+ self.init_app(app)
73
+
74
+ def init_app(self, app: FastAPI) -> None:
75
+ """
76
+ Initialize with FastAPI app.
77
+
78
+ Stores client in app.state for access in routes.
79
+ """
80
+ app.state.x402_client = self._client
81
+ app.state.x402_config = self._config
82
+
83
+ @property
84
+ def client(self) -> X402Client:
85
+ """Get the x402 client."""
86
+ return self._client
87
+
88
+ @property
89
+ def config(self) -> X402Config:
90
+ """Get the x402 config."""
91
+ return self._config
92
+
93
+ def require_payment(
94
+ self,
95
+ amount_usd: Union[Decimal, float, str],
96
+ message: Optional[str] = None,
97
+ ) -> Callable[..., PaymentResult]:
98
+ """
99
+ Create a FastAPI dependency that requires payment.
100
+
101
+ Args:
102
+ amount_usd: Required payment amount in USD
103
+ message: Custom message for 402 response
104
+
105
+ Returns:
106
+ Dependency function that returns PaymentResult
107
+
108
+ Example:
109
+ >>> @app.post("/api/premium")
110
+ >>> async def premium(
111
+ ... request: Request,
112
+ ... payment: PaymentResult = Depends(x402.require_payment(5.00))
113
+ ... ):
114
+ ... return {"payer": payment.payer_address}
115
+ """
116
+ required_amount = Decimal(str(amount_usd))
117
+
118
+ async def dependency(request: Request) -> PaymentResult:
119
+ payment_header = request.headers.get("X-PAYMENT")
120
+
121
+ if not payment_header:
122
+ response_body = create_402_response(
123
+ amount_usd=required_amount,
124
+ config=self._config,
125
+ message=message,
126
+ )
127
+ raise HTTPException(
128
+ status_code=402,
129
+ detail=response_body,
130
+ headers=create_402_headers(),
131
+ )
132
+
133
+ try:
134
+ return self._client.process_payment(
135
+ x_payment_header=payment_header,
136
+ expected_amount_usd=required_amount,
137
+ )
138
+ except X402Error as e:
139
+ raise HTTPException(
140
+ status_code=402,
141
+ detail=e.to_dict(),
142
+ headers=create_402_headers(),
143
+ )
144
+
145
+ return dependency
146
+
147
+
148
+ class X402Depends:
149
+ """
150
+ Reusable FastAPI dependency for x402 payments.
151
+
152
+ This provides a cleaner syntax for dependency injection.
153
+
154
+ Example:
155
+ >>> x402_payment = X402Depends(
156
+ ... config=X402Config(recipient_evm="0x..."),
157
+ ... amount_usd=Decimal("1.00")
158
+ ... )
159
+ >>>
160
+ >>> @app.get("/resource")
161
+ >>> async def resource(payment: PaymentResult = Depends(x402_payment)):
162
+ ... return {"payer": payment.payer_address}
163
+ """
164
+
165
+ def __init__(
166
+ self,
167
+ config: X402Config,
168
+ amount_usd: Union[Decimal, float, str],
169
+ message: Optional[str] = None,
170
+ ) -> None:
171
+ self._config = config
172
+ self._client = X402Client(config=config)
173
+ self._amount = Decimal(str(amount_usd))
174
+ self._message = message
175
+
176
+ async def __call__(self, request: Request) -> PaymentResult:
177
+ """Process payment when used as dependency."""
178
+ payment_header = request.headers.get("X-PAYMENT")
179
+
180
+ if not payment_header:
181
+ response_body = create_402_response(
182
+ amount_usd=self._amount,
183
+ config=self._config,
184
+ message=self._message,
185
+ )
186
+ raise HTTPException(
187
+ status_code=402,
188
+ detail=response_body,
189
+ headers=create_402_headers(),
190
+ )
191
+
192
+ try:
193
+ return self._client.process_payment(
194
+ x_payment_header=payment_header,
195
+ expected_amount_usd=self._amount,
196
+ )
197
+ except X402Error as e:
198
+ raise HTTPException(
199
+ status_code=402,
200
+ detail=e.to_dict(),
201
+ headers=create_402_headers(),
202
+ )
203
+
204
+
205
+ def fastapi_require_payment(
206
+ amount_usd: Union[Decimal, float, str],
207
+ config: X402Config,
208
+ message: Optional[str] = None,
209
+ ) -> Callable[[F], F]:
210
+ """
211
+ Decorator for FastAPI routes requiring payment.
212
+
213
+ Alternative to dependency injection for simpler cases.
214
+
215
+ Args:
216
+ amount_usd: Required payment amount
217
+ config: X402Config with recipient addresses
218
+ message: Custom 402 message
219
+
220
+ Example:
221
+ >>> @app.get("/resource")
222
+ >>> @fastapi_require_payment(amount_usd="1.00", config=config)
223
+ >>> async def resource(request: Request):
224
+ ... # Payment already verified
225
+ ... return {"success": True}
226
+ """
227
+ required_amount = Decimal(str(amount_usd))
228
+ client = X402Client(config=config)
229
+
230
+ def decorator(func: F) -> F:
231
+ @wraps(func)
232
+ async def wrapper(request: Request, *args: Any, **kwargs: Any) -> Any:
233
+ payment_header = request.headers.get("X-PAYMENT")
234
+
235
+ if not payment_header:
236
+ response_body = create_402_response(
237
+ amount_usd=required_amount,
238
+ config=config,
239
+ message=message,
240
+ )
241
+ return JSONResponse(
242
+ status_code=402,
243
+ content=response_body,
244
+ headers=create_402_headers(),
245
+ )
246
+
247
+ try:
248
+ result = client.process_payment(
249
+ x_payment_header=payment_header,
250
+ expected_amount_usd=required_amount,
251
+ )
252
+ # Store result in request state
253
+ request.state.payment_result = result
254
+ return await func(request, *args, **kwargs)
255
+
256
+ except X402Error as e:
257
+ return JSONResponse(
258
+ status_code=402,
259
+ content=e.to_dict(),
260
+ headers=create_402_headers(),
261
+ )
262
+
263
+ return wrapper # type: ignore
264
+
265
+ return decorator
266
+
267
+
268
+ class X402Middleware(BaseHTTPMiddleware):
269
+ """
270
+ Middleware that automatically handles x402 payments for configured paths.
271
+
272
+ Example:
273
+ >>> from uvd_x402_sdk.integrations.fastapi_integration import X402Middleware
274
+ >>>
275
+ >>> app.add_middleware(
276
+ ... X402Middleware,
277
+ ... config=config,
278
+ ... protected_paths={
279
+ ... "/api/premium": Decimal("5.00"),
280
+ ... "/api/basic": Decimal("1.00"),
281
+ ... }
282
+ ... )
283
+ """
284
+
285
+ def __init__(
286
+ self,
287
+ app: Any,
288
+ config: X402Config,
289
+ protected_paths: dict[str, Decimal],
290
+ ) -> None:
291
+ super().__init__(app)
292
+ self._config = config
293
+ self._client = X402Client(config=config)
294
+ self._protected_paths = protected_paths
295
+
296
+ async def dispatch(self, request: Request, call_next: Any) -> Any:
297
+ path = request.url.path
298
+
299
+ # Check if path is protected
300
+ if path not in self._protected_paths:
301
+ return await call_next(request)
302
+
303
+ required_amount = self._protected_paths[path]
304
+ payment_header = request.headers.get("X-PAYMENT")
305
+
306
+ if not payment_header:
307
+ response_body = create_402_response(
308
+ amount_usd=required_amount,
309
+ config=self._config,
310
+ )
311
+ return JSONResponse(
312
+ status_code=402,
313
+ content=response_body,
314
+ headers=create_402_headers(),
315
+ )
316
+
317
+ try:
318
+ result = self._client.process_payment(
319
+ x_payment_header=payment_header,
320
+ expected_amount_usd=required_amount,
321
+ )
322
+ request.state.payment_result = result
323
+ return await call_next(request)
324
+
325
+ except X402Error as e:
326
+ return JSONResponse(
327
+ status_code=402,
328
+ content=e.to_dict(),
329
+ headers=create_402_headers(),
330
+ )
@@ -0,0 +1,259 @@
1
+ """
2
+ Flask integration for x402 payments.
3
+
4
+ Provides:
5
+ - FlaskX402: Extension for initializing x402 with Flask apps
6
+ - flask_require_payment: Decorator for protecting routes
7
+ """
8
+
9
+ from decimal import Decimal
10
+ from functools import wraps
11
+ from typing import Any, Callable, Optional, TypeVar, Union
12
+
13
+ try:
14
+ from flask import Flask, request, jsonify, make_response, g
15
+ except ImportError:
16
+ raise ImportError(
17
+ "Flask is required for Flask integration. "
18
+ "Install with: pip install uvd-x402-sdk[flask]"
19
+ )
20
+
21
+ from uvd_x402_sdk.client import X402Client
22
+ from uvd_x402_sdk.config import X402Config
23
+ from uvd_x402_sdk.exceptions import X402Error
24
+ from uvd_x402_sdk.response import create_402_response, create_402_headers
25
+
26
+ F = TypeVar("F", bound=Callable[..., Any])
27
+
28
+
29
+ class FlaskX402:
30
+ """
31
+ Flask extension for x402 payment integration.
32
+
33
+ Example:
34
+ >>> from flask import Flask
35
+ >>> from uvd_x402_sdk.integrations import FlaskX402
36
+ >>>
37
+ >>> app = Flask(__name__)
38
+ >>> x402 = FlaskX402(app, recipient_address="0xYourWallet...")
39
+ >>>
40
+ >>> @app.route("/premium")
41
+ >>> @x402.require_payment(amount_usd=Decimal("1.00"))
42
+ >>> def premium():
43
+ ... return {"message": f"Paid by {g.payment_result.payer_address}"}
44
+ """
45
+
46
+ def __init__(
47
+ self,
48
+ app: Optional[Flask] = None,
49
+ config: Optional[X402Config] = None,
50
+ recipient_address: Optional[str] = None,
51
+ **kwargs: Any,
52
+ ) -> None:
53
+ """
54
+ Initialize Flask x402 extension.
55
+
56
+ Args:
57
+ app: Flask application (optional, can use init_app later)
58
+ config: X402Config object
59
+ recipient_address: Default recipient for EVM chains
60
+ **kwargs: Additional config parameters
61
+ """
62
+ self.app = app
63
+ self._config = config
64
+ self._config_kwargs = {"recipient_evm": recipient_address, **kwargs}
65
+ self._client: Optional[X402Client] = None
66
+
67
+ if app is not None:
68
+ self.init_app(app)
69
+
70
+ def init_app(self, app: Flask) -> None:
71
+ """
72
+ Initialize extension with Flask app.
73
+
74
+ Args:
75
+ app: Flask application
76
+ """
77
+ self.app = app
78
+
79
+ # Get config from app config if not provided
80
+ if self._config is None:
81
+ self._config = X402Config(
82
+ facilitator_url=app.config.get(
83
+ "X402_FACILITATOR_URL",
84
+ "https://facilitator.ultravioletadao.xyz",
85
+ ),
86
+ recipient_evm=app.config.get("X402_RECIPIENT_EVM", "")
87
+ or self._config_kwargs.get("recipient_evm", ""),
88
+ recipient_solana=app.config.get("X402_RECIPIENT_SOLANA", ""),
89
+ recipient_near=app.config.get("X402_RECIPIENT_NEAR", ""),
90
+ recipient_stellar=app.config.get("X402_RECIPIENT_STELLAR", ""),
91
+ )
92
+
93
+ self._client = X402Client(config=self._config)
94
+
95
+ # Store extension on app
96
+ if not hasattr(app, "extensions"):
97
+ app.extensions = {}
98
+ app.extensions["x402"] = self
99
+
100
+ @property
101
+ def client(self) -> X402Client:
102
+ """Get the x402 client."""
103
+ if self._client is None:
104
+ raise RuntimeError("FlaskX402 not initialized. Call init_app() first.")
105
+ return self._client
106
+
107
+ @property
108
+ def config(self) -> X402Config:
109
+ """Get the x402 config."""
110
+ if self._config is None:
111
+ raise RuntimeError("FlaskX402 not initialized. Call init_app() first.")
112
+ return self._config
113
+
114
+ def require_payment(
115
+ self,
116
+ amount_usd: Optional[Union[Decimal, float, str]] = None,
117
+ amount_callback: Optional[Callable[[], Decimal]] = None,
118
+ message: Optional[str] = None,
119
+ store_in_g: bool = True,
120
+ ) -> Callable[[F], F]:
121
+ """
122
+ Decorator that requires payment for a Flask route.
123
+
124
+ Args:
125
+ amount_usd: Fixed payment amount in USD
126
+ amount_callback: Callable that returns dynamic amount
127
+ message: Custom message for 402 response
128
+ store_in_g: Store PaymentResult in flask.g.payment_result
129
+
130
+ Returns:
131
+ Decorated route function
132
+
133
+ Example:
134
+ >>> @app.route("/api/premium")
135
+ >>> @x402.require_payment(amount_usd="5.00")
136
+ >>> def premium_endpoint():
137
+ ... return {"payer": g.payment_result.payer_address}
138
+ """
139
+
140
+ def decorator(func: F) -> F:
141
+ @wraps(func)
142
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
143
+ # Get payment header
144
+ payment_header = request.headers.get("X-PAYMENT")
145
+
146
+ # Determine amount
147
+ if amount_callback:
148
+ required_amount = Decimal(str(amount_callback()))
149
+ elif amount_usd:
150
+ required_amount = Decimal(str(amount_usd))
151
+ else:
152
+ raise ValueError("Either amount_usd or amount_callback is required")
153
+
154
+ # No payment header - return 402
155
+ if not payment_header:
156
+ response_body = create_402_response(
157
+ amount_usd=required_amount,
158
+ config=self.config,
159
+ message=message,
160
+ )
161
+ response = make_response(jsonify(response_body), 402)
162
+ for key, value in create_402_headers().items():
163
+ response.headers[key] = value
164
+ return response
165
+
166
+ # Process payment
167
+ try:
168
+ result = self.client.process_payment(
169
+ x_payment_header=payment_header,
170
+ expected_amount_usd=required_amount,
171
+ )
172
+
173
+ # Store result in g
174
+ if store_in_g:
175
+ g.payment_result = result
176
+
177
+ return func(*args, **kwargs)
178
+
179
+ except X402Error as e:
180
+ response_body = create_402_response(
181
+ amount_usd=required_amount,
182
+ config=self.config,
183
+ message=message,
184
+ )
185
+ response_body["error"] = e.message
186
+ response_body["details"] = e.details
187
+ response = make_response(jsonify(response_body), 402)
188
+ for key, value in create_402_headers().items():
189
+ response.headers[key] = value
190
+ return response
191
+
192
+ return wrapper # type: ignore
193
+
194
+ return decorator
195
+
196
+
197
+ def flask_require_payment(
198
+ amount_usd: Union[Decimal, float, str],
199
+ message: Optional[str] = None,
200
+ ) -> Callable[[F], F]:
201
+ """
202
+ Standalone decorator using global Flask x402 extension.
203
+
204
+ This decorator uses the x402 extension stored in current_app.extensions.
205
+
206
+ Example:
207
+ >>> @app.route("/api/resource")
208
+ >>> @flask_require_payment(amount_usd="1.00")
209
+ >>> def resource():
210
+ ... return {"success": True}
211
+ """
212
+
213
+ def decorator(func: F) -> F:
214
+ @wraps(func)
215
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
216
+ from flask import current_app, g
217
+
218
+ # Get extension from app
219
+ if "x402" not in current_app.extensions:
220
+ raise RuntimeError(
221
+ "FlaskX402 not initialized. "
222
+ "Add FlaskX402(app, ...) to your app setup."
223
+ )
224
+
225
+ x402_ext: FlaskX402 = current_app.extensions["x402"]
226
+ required_amount = Decimal(str(amount_usd))
227
+
228
+ # Get payment header
229
+ payment_header = request.headers.get("X-PAYMENT")
230
+
231
+ # No payment - return 402
232
+ if not payment_header:
233
+ response_body = create_402_response(
234
+ amount_usd=required_amount,
235
+ config=x402_ext.config,
236
+ message=message,
237
+ )
238
+ response = make_response(jsonify(response_body), 402)
239
+ for key, value in create_402_headers().items():
240
+ response.headers[key] = value
241
+ return response
242
+
243
+ # Process payment
244
+ try:
245
+ result = x402_ext.client.process_payment(
246
+ x_payment_header=payment_header,
247
+ expected_amount_usd=required_amount,
248
+ )
249
+ g.payment_result = result
250
+ return func(*args, **kwargs)
251
+
252
+ except X402Error as e:
253
+ response_body = e.to_dict()
254
+ response = make_response(jsonify(response_body), 402)
255
+ return response
256
+
257
+ return wrapper # type: ignore
258
+
259
+ return decorator