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,320 @@
1
+ """
2
+ AWS Lambda integration for x402 payments.
3
+
4
+ Provides:
5
+ - LambdaX402: Helper class for Lambda handlers
6
+ - lambda_handler: Decorator for protecting Lambda functions
7
+ """
8
+
9
+ import json
10
+ import logging
11
+ from decimal import Decimal
12
+ from functools import wraps
13
+ from typing import Any, Callable, Dict, Optional, TypeVar, Union
14
+
15
+ from uvd_x402_sdk.client import X402Client
16
+ from uvd_x402_sdk.config import X402Config
17
+ from uvd_x402_sdk.exceptions import X402Error
18
+ from uvd_x402_sdk.models import PaymentResult
19
+ from uvd_x402_sdk.response import create_402_response, create_402_headers
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+ F = TypeVar("F", bound=Callable[..., Any])
24
+
25
+ # Lambda event/response types
26
+ LambdaEvent = Dict[str, Any]
27
+ LambdaContext = Any
28
+ LambdaResponse = Dict[str, Any]
29
+
30
+
31
+ def _get_header(event: LambdaEvent, header_name: str) -> Optional[str]:
32
+ """
33
+ Get header from Lambda event.
34
+
35
+ Handles both API Gateway REST API and HTTP API formats.
36
+ """
37
+ headers = event.get("headers", {})
38
+ if not headers:
39
+ return None
40
+
41
+ # Try exact match
42
+ if header_name in headers:
43
+ return headers[header_name]
44
+
45
+ # Try lowercase (HTTP API normalizes to lowercase)
46
+ lower_name = header_name.lower()
47
+ if lower_name in headers:
48
+ return headers[lower_name]
49
+
50
+ # Try case-insensitive search
51
+ for key, value in headers.items():
52
+ if key.lower() == lower_name:
53
+ return value
54
+
55
+ return None
56
+
57
+
58
+ def _create_lambda_response(
59
+ status_code: int,
60
+ body: Any,
61
+ headers: Optional[Dict[str, str]] = None,
62
+ ) -> LambdaResponse:
63
+ """Create a Lambda response in API Gateway format."""
64
+ response: LambdaResponse = {
65
+ "statusCode": status_code,
66
+ "headers": {
67
+ "Content-Type": "application/json",
68
+ "Access-Control-Allow-Origin": "*",
69
+ "Access-Control-Allow-Headers": "Content-Type,X-PAYMENT,Authorization",
70
+ **(headers or {}),
71
+ },
72
+ }
73
+
74
+ if isinstance(body, str):
75
+ response["body"] = body
76
+ else:
77
+ response["body"] = json.dumps(body)
78
+
79
+ return response
80
+
81
+
82
+ class LambdaX402:
83
+ """
84
+ Helper class for x402 payments in AWS Lambda.
85
+
86
+ Example:
87
+ >>> from uvd_x402_sdk.integrations import LambdaX402
88
+ >>>
89
+ >>> x402 = LambdaX402(
90
+ ... recipient_evm="0xYourWallet...",
91
+ ... recipient_solana="YourSolanaWallet...",
92
+ ... )
93
+ >>>
94
+ >>> def handler(event, context):
95
+ ... # Check for payment requirement
96
+ ... body = json.loads(event.get("body", "{}"))
97
+ ... price = calculate_price(body)
98
+ ...
99
+ ... # Process payment
100
+ ... result = x402.process_or_require(event, price)
101
+ ...
102
+ ... # If result is a response dict, payment is required
103
+ ... if "statusCode" in result:
104
+ ... return result
105
+ ...
106
+ ... # Payment verified - result is PaymentResult
107
+ ... return {
108
+ ... "statusCode": 200,
109
+ ... "body": json.dumps({"payer": result.payer_address})
110
+ ... }
111
+ """
112
+
113
+ def __init__(
114
+ self,
115
+ config: Optional[X402Config] = None,
116
+ recipient_evm: str = "",
117
+ recipient_solana: str = "",
118
+ recipient_near: str = "",
119
+ recipient_stellar: str = "",
120
+ **kwargs: Any,
121
+ ) -> None:
122
+ """
123
+ Initialize Lambda x402 helper.
124
+
125
+ Args:
126
+ config: X402Config object
127
+ recipient_evm: EVM recipient address
128
+ recipient_solana: Solana recipient address
129
+ recipient_near: NEAR recipient account
130
+ recipient_stellar: Stellar recipient address
131
+ **kwargs: Additional config parameters
132
+ """
133
+ if config:
134
+ self._config = config
135
+ else:
136
+ self._config = X402Config(
137
+ recipient_evm=recipient_evm,
138
+ recipient_solana=recipient_solana,
139
+ recipient_near=recipient_near,
140
+ recipient_stellar=recipient_stellar,
141
+ **kwargs,
142
+ )
143
+ self._client = X402Client(config=self._config)
144
+
145
+ @property
146
+ def client(self) -> X402Client:
147
+ """Get the x402 client."""
148
+ return self._client
149
+
150
+ @property
151
+ def config(self) -> X402Config:
152
+ """Get the x402 config."""
153
+ return self._config
154
+
155
+ def get_payment_header(self, event: LambdaEvent) -> Optional[str]:
156
+ """Get X-PAYMENT header from Lambda event."""
157
+ return _get_header(event, "X-PAYMENT")
158
+
159
+ def create_402_response(
160
+ self,
161
+ amount_usd: Union[Decimal, float, str],
162
+ message: Optional[str] = None,
163
+ ) -> LambdaResponse:
164
+ """
165
+ Create a 402 Payment Required response.
166
+
167
+ Args:
168
+ amount_usd: Required payment amount
169
+ message: Custom message
170
+
171
+ Returns:
172
+ Lambda response dict with 402 status
173
+ """
174
+ body = create_402_response(
175
+ amount_usd=Decimal(str(amount_usd)),
176
+ config=self._config,
177
+ message=message,
178
+ )
179
+ return _create_lambda_response(402, body, create_402_headers())
180
+
181
+ def process_payment(
182
+ self,
183
+ event: LambdaEvent,
184
+ expected_amount_usd: Union[Decimal, float, str],
185
+ ) -> PaymentResult:
186
+ """
187
+ Process x402 payment from Lambda event.
188
+
189
+ Args:
190
+ event: Lambda event containing X-PAYMENT header
191
+ expected_amount_usd: Expected payment amount
192
+
193
+ Returns:
194
+ PaymentResult on success
195
+
196
+ Raises:
197
+ X402Error: If payment verification/settlement fails
198
+ """
199
+ payment_header = self.get_payment_header(event)
200
+ if not payment_header:
201
+ raise X402Error("Missing X-PAYMENT header", code="PAYMENT_REQUIRED")
202
+
203
+ return self._client.process_payment(
204
+ x_payment_header=payment_header,
205
+ expected_amount_usd=Decimal(str(expected_amount_usd)),
206
+ )
207
+
208
+ def process_or_require(
209
+ self,
210
+ event: LambdaEvent,
211
+ amount_usd: Union[Decimal, float, str],
212
+ message: Optional[str] = None,
213
+ ) -> Union[PaymentResult, LambdaResponse]:
214
+ """
215
+ Process payment or return 402 response.
216
+
217
+ This is the main method for Lambda handlers. It:
218
+ 1. Checks for X-PAYMENT header
219
+ 2. If missing, returns 402 response
220
+ 3. If present, processes payment and returns PaymentResult
221
+
222
+ Args:
223
+ event: Lambda event
224
+ amount_usd: Required payment amount
225
+ message: Custom 402 message
226
+
227
+ Returns:
228
+ PaymentResult if payment verified, LambdaResponse if 402 needed
229
+ """
230
+ payment_header = self.get_payment_header(event)
231
+
232
+ if not payment_header:
233
+ logger.info(f"No payment header, returning 402 for ${amount_usd}")
234
+ return self.create_402_response(amount_usd, message)
235
+
236
+ try:
237
+ result = self._client.process_payment(
238
+ x_payment_header=payment_header,
239
+ expected_amount_usd=Decimal(str(amount_usd)),
240
+ )
241
+ logger.info(f"Payment processed: {result.payer_address} paid ${amount_usd}")
242
+ return result
243
+
244
+ except X402Error as e:
245
+ logger.warning(f"Payment failed: {e.message}")
246
+ body = create_402_response(
247
+ amount_usd=Decimal(str(amount_usd)),
248
+ config=self._config,
249
+ message=message,
250
+ )
251
+ body["error"] = e.message
252
+ body["details"] = e.details
253
+ return _create_lambda_response(402, body, create_402_headers())
254
+
255
+
256
+ def lambda_handler(
257
+ amount_usd: Optional[Union[Decimal, float, str]] = None,
258
+ amount_callback: Optional[Callable[[LambdaEvent], Decimal]] = None,
259
+ config: Optional[X402Config] = None,
260
+ recipient_address: Optional[str] = None,
261
+ message: Optional[str] = None,
262
+ ) -> Callable[[F], F]:
263
+ """
264
+ Decorator for Lambda handlers requiring x402 payment.
265
+
266
+ The decorator handles the payment flow and injects PaymentResult
267
+ into the handler's kwargs.
268
+
269
+ Args:
270
+ amount_usd: Fixed payment amount
271
+ amount_callback: Function to calculate dynamic amount from event
272
+ config: X402Config object
273
+ recipient_address: EVM recipient (convenience arg)
274
+ message: Custom 402 message
275
+
276
+ Example (fixed amount):
277
+ >>> @lambda_handler(amount_usd="1.00", recipient_address="0x...")
278
+ >>> def handler(event, context, payment_result=None):
279
+ ... return {
280
+ ... "statusCode": 200,
281
+ ... "body": json.dumps({"payer": payment_result.payer_address})
282
+ ... }
283
+
284
+ Example (dynamic pricing):
285
+ >>> def calculate_price(event):
286
+ ... body = json.loads(event.get("body", "{}"))
287
+ ... pixels = body.get("pixels", 1)
288
+ ... return Decimal(str(pixels * 0.01)) # $0.01 per pixel
289
+ >>>
290
+ >>> @lambda_handler(amount_callback=calculate_price, recipient_address="0x...")
291
+ >>> def handler(event, context, payment_result=None):
292
+ ... return {"statusCode": 200, "body": "..."}
293
+ """
294
+ _config = config or X402Config(recipient_evm=recipient_address or "")
295
+ x402 = LambdaX402(config=_config)
296
+
297
+ def decorator(func: F) -> F:
298
+ @wraps(func)
299
+ def wrapper(event: LambdaEvent, context: LambdaContext) -> LambdaResponse:
300
+ # Determine amount
301
+ if amount_callback:
302
+ required_amount = amount_callback(event)
303
+ elif amount_usd:
304
+ required_amount = Decimal(str(amount_usd))
305
+ else:
306
+ raise ValueError("Either amount_usd or amount_callback is required")
307
+
308
+ # Process or require payment
309
+ result = x402.process_or_require(event, required_amount, message)
310
+
311
+ # If it's a response dict, return it (402)
312
+ if isinstance(result, dict) and "statusCode" in result:
313
+ return result
314
+
315
+ # Payment verified - call handler
316
+ return func(event, context, payment_result=result)
317
+
318
+ return wrapper # type: ignore
319
+
320
+ return decorator