uvd-x402-sdk 0.2.2__py3-none-any.whl → 0.3.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.
- uvd_x402_sdk/__init__.py +185 -169
- uvd_x402_sdk/client.py +527 -527
- uvd_x402_sdk/config.py +248 -248
- uvd_x402_sdk/decorators.py +325 -325
- uvd_x402_sdk/exceptions.py +254 -254
- uvd_x402_sdk/integrations/__init__.py +74 -74
- uvd_x402_sdk/integrations/django_integration.py +237 -237
- uvd_x402_sdk/integrations/fastapi_integration.py +330 -330
- uvd_x402_sdk/integrations/flask_integration.py +259 -259
- uvd_x402_sdk/integrations/lambda_integration.py +320 -320
- uvd_x402_sdk/models.py +397 -397
- uvd_x402_sdk/networks/__init__.py +80 -54
- uvd_x402_sdk/networks/base.py +498 -347
- uvd_x402_sdk/networks/evm.py +356 -215
- uvd_x402_sdk/networks/near.py +397 -397
- uvd_x402_sdk/networks/solana.py +282 -282
- uvd_x402_sdk/networks/stellar.py +129 -129
- uvd_x402_sdk/response.py +439 -439
- {uvd_x402_sdk-0.2.2.dist-info → uvd_x402_sdk-0.3.0.dist-info}/LICENSE +21 -21
- {uvd_x402_sdk-0.2.2.dist-info → uvd_x402_sdk-0.3.0.dist-info}/METADATA +927 -778
- uvd_x402_sdk-0.3.0.dist-info/RECORD +23 -0
- uvd_x402_sdk-0.2.2.dist-info/RECORD +0 -23
- {uvd_x402_sdk-0.2.2.dist-info → uvd_x402_sdk-0.3.0.dist-info}/WHEEL +0 -0
- {uvd_x402_sdk-0.2.2.dist-info → uvd_x402_sdk-0.3.0.dist-info}/top_level.txt +0 -0
|
@@ -1,320 +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
|
|
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
|