uvd-x402-sdk 0.2.2__py3-none-any.whl → 0.2.3__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 +169 -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 +54 -54
- uvd_x402_sdk/networks/base.py +347 -347
- uvd_x402_sdk/networks/evm.py +215 -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.2.3.dist-info}/LICENSE +21 -21
- {uvd_x402_sdk-0.2.2.dist-info → uvd_x402_sdk-0.2.3.dist-info}/METADATA +814 -778
- uvd_x402_sdk-0.2.3.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.2.3.dist-info}/WHEEL +0 -0
- {uvd_x402_sdk-0.2.2.dist-info → uvd_x402_sdk-0.2.3.dist-info}/top_level.txt +0 -0
uvd_x402_sdk/client.py
CHANGED
|
@@ -1,527 +1,527 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Main x402 client for payment processing.
|
|
3
|
-
|
|
4
|
-
This module provides the X402Client class which handles:
|
|
5
|
-
- Parsing X-PAYMENT headers
|
|
6
|
-
- Verifying payments with the facilitator
|
|
7
|
-
- Settling payments on-chain
|
|
8
|
-
- Error handling with clear messages
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
import base64
|
|
12
|
-
import json
|
|
13
|
-
import logging
|
|
14
|
-
from decimal import Decimal
|
|
15
|
-
from typing import Optional, Tuple, Dict, Any
|
|
16
|
-
|
|
17
|
-
import httpx
|
|
18
|
-
|
|
19
|
-
from uvd_x402_sdk.config import X402Config
|
|
20
|
-
from uvd_x402_sdk.exceptions import (
|
|
21
|
-
InvalidPayloadError,
|
|
22
|
-
PaymentVerificationError,
|
|
23
|
-
PaymentSettlementError,
|
|
24
|
-
UnsupportedNetworkError,
|
|
25
|
-
FacilitatorError,
|
|
26
|
-
TimeoutError as X402TimeoutError,
|
|
27
|
-
)
|
|
28
|
-
from uvd_x402_sdk.models import (
|
|
29
|
-
PaymentPayload,
|
|
30
|
-
PaymentRequirements,
|
|
31
|
-
PaymentResult,
|
|
32
|
-
VerifyResponse,
|
|
33
|
-
SettleResponse,
|
|
34
|
-
)
|
|
35
|
-
from uvd_x402_sdk.networks import (
|
|
36
|
-
get_network,
|
|
37
|
-
NetworkType,
|
|
38
|
-
get_supported_network_names,
|
|
39
|
-
normalize_network,
|
|
40
|
-
is_caip2_format,
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
logger = logging.getLogger(__name__)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
class X402Client:
|
|
47
|
-
"""
|
|
48
|
-
Client for processing x402 payments via the Ultravioleta facilitator.
|
|
49
|
-
|
|
50
|
-
The client handles the two-step payment flow:
|
|
51
|
-
1. Verify: Validate the payment signature/authorization
|
|
52
|
-
2. Settle: Execute the payment on-chain
|
|
53
|
-
|
|
54
|
-
Example:
|
|
55
|
-
>>> client = X402Client(
|
|
56
|
-
... recipient_address="0xYourWallet...",
|
|
57
|
-
... facilitator_url="https://facilitator.ultravioletadao.xyz"
|
|
58
|
-
... )
|
|
59
|
-
>>> result = client.process_payment(
|
|
60
|
-
... x_payment_header=request.headers.get("X-PAYMENT"),
|
|
61
|
-
... expected_amount_usd=Decimal("10.00")
|
|
62
|
-
... )
|
|
63
|
-
>>> print(f"Paid by {result.payer_address}, tx: {result.transaction_hash}")
|
|
64
|
-
"""
|
|
65
|
-
|
|
66
|
-
def __init__(
|
|
67
|
-
self,
|
|
68
|
-
recipient_address: Optional[str] = None,
|
|
69
|
-
facilitator_url: str = "https://facilitator.ultravioletadao.xyz",
|
|
70
|
-
config: Optional[X402Config] = None,
|
|
71
|
-
**kwargs: Any,
|
|
72
|
-
) -> None:
|
|
73
|
-
"""
|
|
74
|
-
Initialize the x402 client.
|
|
75
|
-
|
|
76
|
-
Args:
|
|
77
|
-
recipient_address: Default recipient for EVM chains (convenience arg)
|
|
78
|
-
facilitator_url: URL of the facilitator service
|
|
79
|
-
config: Full X402Config object (overrides other args)
|
|
80
|
-
**kwargs: Additional config parameters passed to X402Config
|
|
81
|
-
|
|
82
|
-
Raises:
|
|
83
|
-
ValueError: If no recipient address is configured
|
|
84
|
-
"""
|
|
85
|
-
if config:
|
|
86
|
-
self.config = config
|
|
87
|
-
else:
|
|
88
|
-
# Build config from individual args
|
|
89
|
-
config_kwargs = {
|
|
90
|
-
"facilitator_url": facilitator_url,
|
|
91
|
-
"recipient_evm": recipient_address or kwargs.get("recipient_evm", ""),
|
|
92
|
-
**kwargs,
|
|
93
|
-
}
|
|
94
|
-
# Remove None values
|
|
95
|
-
config_kwargs = {k: v for k, v in config_kwargs.items() if v is not None}
|
|
96
|
-
self.config = X402Config(**config_kwargs)
|
|
97
|
-
|
|
98
|
-
# HTTP client for facilitator requests
|
|
99
|
-
self._http_client: Optional[httpx.Client] = None
|
|
100
|
-
|
|
101
|
-
def _get_http_client(self) -> httpx.Client:
|
|
102
|
-
"""Get or create HTTP client."""
|
|
103
|
-
if self._http_client is None or self._http_client.is_closed:
|
|
104
|
-
self._http_client = httpx.Client(
|
|
105
|
-
timeout=httpx.Timeout(
|
|
106
|
-
connect=10.0,
|
|
107
|
-
read=self.config.settle_timeout,
|
|
108
|
-
write=10.0,
|
|
109
|
-
pool=10.0,
|
|
110
|
-
)
|
|
111
|
-
)
|
|
112
|
-
return self._http_client
|
|
113
|
-
|
|
114
|
-
def close(self) -> None:
|
|
115
|
-
"""Close the HTTP client."""
|
|
116
|
-
if self._http_client:
|
|
117
|
-
self._http_client.close()
|
|
118
|
-
self._http_client = None
|
|
119
|
-
|
|
120
|
-
def __enter__(self) -> "X402Client":
|
|
121
|
-
return self
|
|
122
|
-
|
|
123
|
-
def __exit__(self, *args: Any) -> None:
|
|
124
|
-
self.close()
|
|
125
|
-
|
|
126
|
-
# =========================================================================
|
|
127
|
-
# Payload Parsing
|
|
128
|
-
# =========================================================================
|
|
129
|
-
|
|
130
|
-
def extract_payload(self, x_payment_header: str) -> PaymentPayload:
|
|
131
|
-
"""
|
|
132
|
-
Extract and validate payment payload from X-PAYMENT header.
|
|
133
|
-
|
|
134
|
-
Args:
|
|
135
|
-
x_payment_header: Base64-encoded JSON payload
|
|
136
|
-
|
|
137
|
-
Returns:
|
|
138
|
-
Parsed PaymentPayload object
|
|
139
|
-
|
|
140
|
-
Raises:
|
|
141
|
-
InvalidPayloadError: If payload is invalid
|
|
142
|
-
"""
|
|
143
|
-
if not x_payment_header:
|
|
144
|
-
raise InvalidPayloadError("Missing X-PAYMENT header")
|
|
145
|
-
|
|
146
|
-
try:
|
|
147
|
-
# Decode base64
|
|
148
|
-
json_bytes = base64.b64decode(x_payment_header)
|
|
149
|
-
json_str = json_bytes.decode("utf-8")
|
|
150
|
-
|
|
151
|
-
# Parse JSON
|
|
152
|
-
data = json.loads(json_str)
|
|
153
|
-
|
|
154
|
-
# Validate and parse with Pydantic
|
|
155
|
-
payload = PaymentPayload(**data)
|
|
156
|
-
|
|
157
|
-
logger.debug(f"Extracted payload for network: {payload.network}")
|
|
158
|
-
return payload
|
|
159
|
-
|
|
160
|
-
except base64.binascii.Error as e:
|
|
161
|
-
raise InvalidPayloadError(f"Invalid base64 encoding: {e}")
|
|
162
|
-
except json.JSONDecodeError as e:
|
|
163
|
-
raise InvalidPayloadError(f"Invalid JSON in payload: {e}")
|
|
164
|
-
except Exception as e:
|
|
165
|
-
raise InvalidPayloadError(f"Failed to parse payload: {e}")
|
|
166
|
-
|
|
167
|
-
# =========================================================================
|
|
168
|
-
# Network Validation
|
|
169
|
-
# =========================================================================
|
|
170
|
-
|
|
171
|
-
def validate_network(self, network: str) -> str:
|
|
172
|
-
"""
|
|
173
|
-
Validate that a network is supported and enabled.
|
|
174
|
-
|
|
175
|
-
Handles both v1 ("base") and v2 CAIP-2 ("eip155:8453") formats.
|
|
176
|
-
|
|
177
|
-
Args:
|
|
178
|
-
network: Network identifier (v1 or CAIP-2)
|
|
179
|
-
|
|
180
|
-
Returns:
|
|
181
|
-
Normalized network name
|
|
182
|
-
|
|
183
|
-
Raises:
|
|
184
|
-
UnsupportedNetworkError: If network is not supported
|
|
185
|
-
"""
|
|
186
|
-
# Normalize CAIP-2 to network name
|
|
187
|
-
try:
|
|
188
|
-
normalized = normalize_network(network)
|
|
189
|
-
except ValueError:
|
|
190
|
-
raise UnsupportedNetworkError(
|
|
191
|
-
network=network,
|
|
192
|
-
supported_networks=get_supported_network_names(),
|
|
193
|
-
)
|
|
194
|
-
|
|
195
|
-
network_config = get_network(normalized)
|
|
196
|
-
if not network_config:
|
|
197
|
-
raise UnsupportedNetworkError(
|
|
198
|
-
network=network,
|
|
199
|
-
supported_networks=get_supported_network_names(),
|
|
200
|
-
)
|
|
201
|
-
|
|
202
|
-
if not network_config.enabled:
|
|
203
|
-
raise UnsupportedNetworkError(
|
|
204
|
-
network=network,
|
|
205
|
-
supported_networks=[n for n in get_supported_network_names()
|
|
206
|
-
if get_network(n) and get_network(n).enabled],
|
|
207
|
-
)
|
|
208
|
-
|
|
209
|
-
if not self.config.is_network_enabled(normalized):
|
|
210
|
-
raise UnsupportedNetworkError(
|
|
211
|
-
network=network,
|
|
212
|
-
supported_networks=self.config.supported_networks,
|
|
213
|
-
)
|
|
214
|
-
|
|
215
|
-
return normalized
|
|
216
|
-
|
|
217
|
-
# =========================================================================
|
|
218
|
-
# Payment Requirements Building
|
|
219
|
-
# =========================================================================
|
|
220
|
-
|
|
221
|
-
def _build_payment_requirements(
|
|
222
|
-
self,
|
|
223
|
-
payload: PaymentPayload,
|
|
224
|
-
expected_amount_usd: Decimal,
|
|
225
|
-
) -> PaymentRequirements:
|
|
226
|
-
"""
|
|
227
|
-
Build payment requirements for facilitator request.
|
|
228
|
-
|
|
229
|
-
Args:
|
|
230
|
-
payload: Parsed payment payload
|
|
231
|
-
expected_amount_usd: Expected payment amount in USD
|
|
232
|
-
|
|
233
|
-
Returns:
|
|
234
|
-
PaymentRequirements object
|
|
235
|
-
"""
|
|
236
|
-
# Normalize network name (handles CAIP-2 format)
|
|
237
|
-
normalized_network = payload.get_normalized_network()
|
|
238
|
-
|
|
239
|
-
network_config = get_network(normalized_network)
|
|
240
|
-
if not network_config:
|
|
241
|
-
raise UnsupportedNetworkError(
|
|
242
|
-
network=payload.network,
|
|
243
|
-
supported_networks=get_supported_network_names(),
|
|
244
|
-
)
|
|
245
|
-
|
|
246
|
-
# Convert USD to token amount
|
|
247
|
-
expected_amount_wei = network_config.get_token_amount(float(expected_amount_usd))
|
|
248
|
-
|
|
249
|
-
# Get recipient for this network
|
|
250
|
-
recipient = self.config.get_recipient(normalized_network)
|
|
251
|
-
|
|
252
|
-
# Build base requirements
|
|
253
|
-
# Use original network format (v1 or v2) for facilitator
|
|
254
|
-
requirements = PaymentRequirements(
|
|
255
|
-
scheme="exact",
|
|
256
|
-
network=payload.network, # Preserve original format
|
|
257
|
-
maxAmountRequired=str(expected_amount_wei),
|
|
258
|
-
resource=self.config.resource_url or f"https://api.example.com/payment",
|
|
259
|
-
description=self.config.description,
|
|
260
|
-
mimeType="application/json",
|
|
261
|
-
payTo=recipient,
|
|
262
|
-
maxTimeoutSeconds=60,
|
|
263
|
-
asset=network_config.usdc_address,
|
|
264
|
-
)
|
|
265
|
-
|
|
266
|
-
# Add EIP-712 domain params for EVM chains
|
|
267
|
-
if network_config.network_type == NetworkType.EVM:
|
|
268
|
-
requirements.extra = {
|
|
269
|
-
"name": network_config.usdc_domain_name,
|
|
270
|
-
"version": network_config.usdc_domain_version,
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
return requirements
|
|
274
|
-
|
|
275
|
-
# =========================================================================
|
|
276
|
-
# Facilitator Communication
|
|
277
|
-
# =========================================================================
|
|
278
|
-
|
|
279
|
-
def verify_payment(
|
|
280
|
-
self,
|
|
281
|
-
payload: PaymentPayload,
|
|
282
|
-
expected_amount_usd: Decimal,
|
|
283
|
-
) -> VerifyResponse:
|
|
284
|
-
"""
|
|
285
|
-
Verify payment with the facilitator.
|
|
286
|
-
|
|
287
|
-
This validates the signature/authorization without settling on-chain.
|
|
288
|
-
|
|
289
|
-
Args:
|
|
290
|
-
payload: Parsed payment payload
|
|
291
|
-
expected_amount_usd: Expected payment amount in USD
|
|
292
|
-
|
|
293
|
-
Returns:
|
|
294
|
-
VerifyResponse from facilitator
|
|
295
|
-
|
|
296
|
-
Raises:
|
|
297
|
-
PaymentVerificationError: If verification fails
|
|
298
|
-
FacilitatorError: If facilitator returns an error
|
|
299
|
-
TimeoutError: If request times out
|
|
300
|
-
"""
|
|
301
|
-
normalized_network = self.validate_network(payload.network)
|
|
302
|
-
requirements = self._build_payment_requirements(payload, expected_amount_usd)
|
|
303
|
-
|
|
304
|
-
verify_request = {
|
|
305
|
-
"x402Version": 1,
|
|
306
|
-
"paymentPayload": payload.model_dump(by_alias=True),
|
|
307
|
-
"paymentRequirements": requirements.model_dump(by_alias=True, exclude_none=True),
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
logger.info(f"Verifying payment on {payload.network} for ${expected_amount_usd}")
|
|
311
|
-
logger.debug(f"Verify request: {json.dumps(verify_request, indent=2)}")
|
|
312
|
-
|
|
313
|
-
try:
|
|
314
|
-
client = self._get_http_client()
|
|
315
|
-
response = client.post(
|
|
316
|
-
f"{self.config.facilitator_url}/verify",
|
|
317
|
-
json=verify_request,
|
|
318
|
-
headers={"Content-Type": "application/json"},
|
|
319
|
-
timeout=self.config.verify_timeout,
|
|
320
|
-
)
|
|
321
|
-
|
|
322
|
-
if response.status_code != 200:
|
|
323
|
-
raise FacilitatorError(
|
|
324
|
-
message=f"Facilitator verify failed with status {response.status_code}",
|
|
325
|
-
status_code=response.status_code,
|
|
326
|
-
response_body=response.text,
|
|
327
|
-
)
|
|
328
|
-
|
|
329
|
-
data = response.json()
|
|
330
|
-
verify_response = VerifyResponse(**data)
|
|
331
|
-
|
|
332
|
-
if not verify_response.isValid:
|
|
333
|
-
raise PaymentVerificationError(
|
|
334
|
-
message=f"Payment verification failed: {verify_response.message}",
|
|
335
|
-
reason=verify_response.invalidReason,
|
|
336
|
-
errors=verify_response.errors,
|
|
337
|
-
)
|
|
338
|
-
|
|
339
|
-
logger.info(f"Payment verified! Payer: {verify_response.payer}")
|
|
340
|
-
return verify_response
|
|
341
|
-
|
|
342
|
-
except httpx.TimeoutException:
|
|
343
|
-
raise X402TimeoutError(operation="verify", timeout_seconds=self.config.verify_timeout)
|
|
344
|
-
except httpx.RequestError as e:
|
|
345
|
-
raise FacilitatorError(message=f"Facilitator request failed: {e}")
|
|
346
|
-
|
|
347
|
-
def settle_payment(
|
|
348
|
-
self,
|
|
349
|
-
payload: PaymentPayload,
|
|
350
|
-
expected_amount_usd: Decimal,
|
|
351
|
-
) -> SettleResponse:
|
|
352
|
-
"""
|
|
353
|
-
Settle payment on-chain via the facilitator.
|
|
354
|
-
|
|
355
|
-
This executes the actual on-chain transfer.
|
|
356
|
-
|
|
357
|
-
Args:
|
|
358
|
-
payload: Parsed payment payload
|
|
359
|
-
expected_amount_usd: Expected payment amount in USD
|
|
360
|
-
|
|
361
|
-
Returns:
|
|
362
|
-
SettleResponse from facilitator
|
|
363
|
-
|
|
364
|
-
Raises:
|
|
365
|
-
PaymentSettlementError: If settlement fails
|
|
366
|
-
FacilitatorError: If facilitator returns an error
|
|
367
|
-
TimeoutError: If request times out
|
|
368
|
-
"""
|
|
369
|
-
normalized_network = self.validate_network(payload.network)
|
|
370
|
-
requirements = self._build_payment_requirements(payload, expected_amount_usd)
|
|
371
|
-
|
|
372
|
-
settle_request = {
|
|
373
|
-
"x402Version": 1,
|
|
374
|
-
"paymentPayload": payload.model_dump(by_alias=True),
|
|
375
|
-
"paymentRequirements": requirements.model_dump(by_alias=True, exclude_none=True),
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
logger.info(f"Settling payment on {payload.network} for ${expected_amount_usd}")
|
|
379
|
-
logger.debug(f"Settle request: {json.dumps(settle_request, indent=2)}")
|
|
380
|
-
|
|
381
|
-
try:
|
|
382
|
-
client = self._get_http_client()
|
|
383
|
-
response = client.post(
|
|
384
|
-
f"{self.config.facilitator_url}/settle",
|
|
385
|
-
json=settle_request,
|
|
386
|
-
headers={"Content-Type": "application/json"},
|
|
387
|
-
timeout=self.config.settle_timeout,
|
|
388
|
-
)
|
|
389
|
-
|
|
390
|
-
if response.status_code != 200:
|
|
391
|
-
raise FacilitatorError(
|
|
392
|
-
message=f"Facilitator settle failed with status {response.status_code}",
|
|
393
|
-
status_code=response.status_code,
|
|
394
|
-
response_body=response.text,
|
|
395
|
-
)
|
|
396
|
-
|
|
397
|
-
data = response.json()
|
|
398
|
-
settle_response = SettleResponse(**data)
|
|
399
|
-
|
|
400
|
-
if not settle_response.success:
|
|
401
|
-
raise PaymentSettlementError(
|
|
402
|
-
message=f"Payment settlement failed: {settle_response.message}",
|
|
403
|
-
network=payload.network,
|
|
404
|
-
reason=settle_response.message,
|
|
405
|
-
)
|
|
406
|
-
|
|
407
|
-
tx_hash = settle_response.get_transaction_hash()
|
|
408
|
-
logger.info(f"Payment settled! TX: {tx_hash}, Payer: {settle_response.payer}")
|
|
409
|
-
return settle_response
|
|
410
|
-
|
|
411
|
-
except httpx.TimeoutException:
|
|
412
|
-
raise X402TimeoutError(operation="settle", timeout_seconds=self.config.settle_timeout)
|
|
413
|
-
except httpx.RequestError as e:
|
|
414
|
-
raise FacilitatorError(message=f"Facilitator request failed: {e}")
|
|
415
|
-
|
|
416
|
-
# =========================================================================
|
|
417
|
-
# Main Processing Method
|
|
418
|
-
# =========================================================================
|
|
419
|
-
|
|
420
|
-
def process_payment(
|
|
421
|
-
self,
|
|
422
|
-
x_payment_header: str,
|
|
423
|
-
expected_amount_usd: Decimal,
|
|
424
|
-
) -> PaymentResult:
|
|
425
|
-
"""
|
|
426
|
-
Process a complete x402 payment (verify + settle).
|
|
427
|
-
|
|
428
|
-
This is the main method for handling payments. It:
|
|
429
|
-
1. Extracts and validates the payment payload
|
|
430
|
-
2. Verifies the payment signature with the facilitator
|
|
431
|
-
3. Settles the payment on-chain
|
|
432
|
-
4. Returns the payment result
|
|
433
|
-
|
|
434
|
-
Args:
|
|
435
|
-
x_payment_header: X-PAYMENT header value (base64-encoded JSON)
|
|
436
|
-
expected_amount_usd: Expected payment amount in USD
|
|
437
|
-
|
|
438
|
-
Returns:
|
|
439
|
-
PaymentResult with payer address, transaction hash, etc.
|
|
440
|
-
|
|
441
|
-
Raises:
|
|
442
|
-
InvalidPayloadError: If payload is invalid
|
|
443
|
-
UnsupportedNetworkError: If network is not supported
|
|
444
|
-
PaymentVerificationError: If verification fails
|
|
445
|
-
PaymentSettlementError: If settlement fails
|
|
446
|
-
FacilitatorError: If facilitator returns an error
|
|
447
|
-
TimeoutError: If request times out
|
|
448
|
-
"""
|
|
449
|
-
# Extract payload
|
|
450
|
-
payload = self.extract_payload(x_payment_header)
|
|
451
|
-
logger.info(f"Processing payment: network={payload.network}, amount=${expected_amount_usd}")
|
|
452
|
-
|
|
453
|
-
# Verify payment
|
|
454
|
-
verify_response = self.verify_payment(payload, expected_amount_usd)
|
|
455
|
-
|
|
456
|
-
# Settle payment
|
|
457
|
-
settle_response = self.settle_payment(payload, expected_amount_usd)
|
|
458
|
-
|
|
459
|
-
# Build result
|
|
460
|
-
return PaymentResult(
|
|
461
|
-
success=True,
|
|
462
|
-
payer_address=settle_response.payer or verify_response.payer or "",
|
|
463
|
-
transaction_hash=settle_response.get_transaction_hash(),
|
|
464
|
-
network=payload.network,
|
|
465
|
-
amount_usd=expected_amount_usd,
|
|
466
|
-
)
|
|
467
|
-
|
|
468
|
-
# =========================================================================
|
|
469
|
-
# Convenience Methods
|
|
470
|
-
# =========================================================================
|
|
471
|
-
|
|
472
|
-
def get_payer_address(self, x_payment_header: str) -> Tuple[str, str]:
|
|
473
|
-
"""
|
|
474
|
-
Extract payer address from payment header without processing.
|
|
475
|
-
|
|
476
|
-
Useful for logging or pre-validation.
|
|
477
|
-
|
|
478
|
-
Args:
|
|
479
|
-
x_payment_header: X-PAYMENT header value
|
|
480
|
-
|
|
481
|
-
Returns:
|
|
482
|
-
Tuple of (payer_address, network)
|
|
483
|
-
"""
|
|
484
|
-
payload = self.extract_payload(x_payment_header)
|
|
485
|
-
|
|
486
|
-
# Normalize network name
|
|
487
|
-
normalized_network = payload.get_normalized_network()
|
|
488
|
-
|
|
489
|
-
# Extract payer based on network type
|
|
490
|
-
network_config = get_network(normalized_network)
|
|
491
|
-
if not network_config:
|
|
492
|
-
raise UnsupportedNetworkError(
|
|
493
|
-
network=payload.network,
|
|
494
|
-
supported_networks=get_supported_network_names(),
|
|
495
|
-
)
|
|
496
|
-
|
|
497
|
-
payer = ""
|
|
498
|
-
if network_config.network_type == NetworkType.EVM:
|
|
499
|
-
evm_payload = payload.get_evm_payload()
|
|
500
|
-
payer = evm_payload.authorization.from_address
|
|
501
|
-
elif network_config.network_type == NetworkType.STELLAR:
|
|
502
|
-
stellar_payload = payload.get_stellar_payload()
|
|
503
|
-
payer = stellar_payload.from_address
|
|
504
|
-
# For SVM/NEAR, payer is determined during verification
|
|
505
|
-
|
|
506
|
-
return payer, normalized_network
|
|
507
|
-
|
|
508
|
-
def verify_only(
|
|
509
|
-
self,
|
|
510
|
-
x_payment_header: str,
|
|
511
|
-
expected_amount_usd: Decimal,
|
|
512
|
-
) -> Tuple[bool, str]:
|
|
513
|
-
"""
|
|
514
|
-
Verify payment without settling.
|
|
515
|
-
|
|
516
|
-
Useful for checking payment validity before committing to settlement.
|
|
517
|
-
|
|
518
|
-
Args:
|
|
519
|
-
x_payment_header: X-PAYMENT header value
|
|
520
|
-
expected_amount_usd: Expected payment amount
|
|
521
|
-
|
|
522
|
-
Returns:
|
|
523
|
-
Tuple of (is_valid, payer_address)
|
|
524
|
-
"""
|
|
525
|
-
payload = self.extract_payload(x_payment_header)
|
|
526
|
-
verify_response = self.verify_payment(payload, expected_amount_usd)
|
|
527
|
-
return verify_response.isValid, verify_response.payer or ""
|
|
1
|
+
"""
|
|
2
|
+
Main x402 client for payment processing.
|
|
3
|
+
|
|
4
|
+
This module provides the X402Client class which handles:
|
|
5
|
+
- Parsing X-PAYMENT headers
|
|
6
|
+
- Verifying payments with the facilitator
|
|
7
|
+
- Settling payments on-chain
|
|
8
|
+
- Error handling with clear messages
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import base64
|
|
12
|
+
import json
|
|
13
|
+
import logging
|
|
14
|
+
from decimal import Decimal
|
|
15
|
+
from typing import Optional, Tuple, Dict, Any
|
|
16
|
+
|
|
17
|
+
import httpx
|
|
18
|
+
|
|
19
|
+
from uvd_x402_sdk.config import X402Config
|
|
20
|
+
from uvd_x402_sdk.exceptions import (
|
|
21
|
+
InvalidPayloadError,
|
|
22
|
+
PaymentVerificationError,
|
|
23
|
+
PaymentSettlementError,
|
|
24
|
+
UnsupportedNetworkError,
|
|
25
|
+
FacilitatorError,
|
|
26
|
+
TimeoutError as X402TimeoutError,
|
|
27
|
+
)
|
|
28
|
+
from uvd_x402_sdk.models import (
|
|
29
|
+
PaymentPayload,
|
|
30
|
+
PaymentRequirements,
|
|
31
|
+
PaymentResult,
|
|
32
|
+
VerifyResponse,
|
|
33
|
+
SettleResponse,
|
|
34
|
+
)
|
|
35
|
+
from uvd_x402_sdk.networks import (
|
|
36
|
+
get_network,
|
|
37
|
+
NetworkType,
|
|
38
|
+
get_supported_network_names,
|
|
39
|
+
normalize_network,
|
|
40
|
+
is_caip2_format,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
logger = logging.getLogger(__name__)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class X402Client:
|
|
47
|
+
"""
|
|
48
|
+
Client for processing x402 payments via the Ultravioleta facilitator.
|
|
49
|
+
|
|
50
|
+
The client handles the two-step payment flow:
|
|
51
|
+
1. Verify: Validate the payment signature/authorization
|
|
52
|
+
2. Settle: Execute the payment on-chain
|
|
53
|
+
|
|
54
|
+
Example:
|
|
55
|
+
>>> client = X402Client(
|
|
56
|
+
... recipient_address="0xYourWallet...",
|
|
57
|
+
... facilitator_url="https://facilitator.ultravioletadao.xyz"
|
|
58
|
+
... )
|
|
59
|
+
>>> result = client.process_payment(
|
|
60
|
+
... x_payment_header=request.headers.get("X-PAYMENT"),
|
|
61
|
+
... expected_amount_usd=Decimal("10.00")
|
|
62
|
+
... )
|
|
63
|
+
>>> print(f"Paid by {result.payer_address}, tx: {result.transaction_hash}")
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
recipient_address: Optional[str] = None,
|
|
69
|
+
facilitator_url: str = "https://facilitator.ultravioletadao.xyz",
|
|
70
|
+
config: Optional[X402Config] = None,
|
|
71
|
+
**kwargs: Any,
|
|
72
|
+
) -> None:
|
|
73
|
+
"""
|
|
74
|
+
Initialize the x402 client.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
recipient_address: Default recipient for EVM chains (convenience arg)
|
|
78
|
+
facilitator_url: URL of the facilitator service
|
|
79
|
+
config: Full X402Config object (overrides other args)
|
|
80
|
+
**kwargs: Additional config parameters passed to X402Config
|
|
81
|
+
|
|
82
|
+
Raises:
|
|
83
|
+
ValueError: If no recipient address is configured
|
|
84
|
+
"""
|
|
85
|
+
if config:
|
|
86
|
+
self.config = config
|
|
87
|
+
else:
|
|
88
|
+
# Build config from individual args
|
|
89
|
+
config_kwargs = {
|
|
90
|
+
"facilitator_url": facilitator_url,
|
|
91
|
+
"recipient_evm": recipient_address or kwargs.get("recipient_evm", ""),
|
|
92
|
+
**kwargs,
|
|
93
|
+
}
|
|
94
|
+
# Remove None values
|
|
95
|
+
config_kwargs = {k: v for k, v in config_kwargs.items() if v is not None}
|
|
96
|
+
self.config = X402Config(**config_kwargs)
|
|
97
|
+
|
|
98
|
+
# HTTP client for facilitator requests
|
|
99
|
+
self._http_client: Optional[httpx.Client] = None
|
|
100
|
+
|
|
101
|
+
def _get_http_client(self) -> httpx.Client:
|
|
102
|
+
"""Get or create HTTP client."""
|
|
103
|
+
if self._http_client is None or self._http_client.is_closed:
|
|
104
|
+
self._http_client = httpx.Client(
|
|
105
|
+
timeout=httpx.Timeout(
|
|
106
|
+
connect=10.0,
|
|
107
|
+
read=self.config.settle_timeout,
|
|
108
|
+
write=10.0,
|
|
109
|
+
pool=10.0,
|
|
110
|
+
)
|
|
111
|
+
)
|
|
112
|
+
return self._http_client
|
|
113
|
+
|
|
114
|
+
def close(self) -> None:
|
|
115
|
+
"""Close the HTTP client."""
|
|
116
|
+
if self._http_client:
|
|
117
|
+
self._http_client.close()
|
|
118
|
+
self._http_client = None
|
|
119
|
+
|
|
120
|
+
def __enter__(self) -> "X402Client":
|
|
121
|
+
return self
|
|
122
|
+
|
|
123
|
+
def __exit__(self, *args: Any) -> None:
|
|
124
|
+
self.close()
|
|
125
|
+
|
|
126
|
+
# =========================================================================
|
|
127
|
+
# Payload Parsing
|
|
128
|
+
# =========================================================================
|
|
129
|
+
|
|
130
|
+
def extract_payload(self, x_payment_header: str) -> PaymentPayload:
|
|
131
|
+
"""
|
|
132
|
+
Extract and validate payment payload from X-PAYMENT header.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
x_payment_header: Base64-encoded JSON payload
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Parsed PaymentPayload object
|
|
139
|
+
|
|
140
|
+
Raises:
|
|
141
|
+
InvalidPayloadError: If payload is invalid
|
|
142
|
+
"""
|
|
143
|
+
if not x_payment_header:
|
|
144
|
+
raise InvalidPayloadError("Missing X-PAYMENT header")
|
|
145
|
+
|
|
146
|
+
try:
|
|
147
|
+
# Decode base64
|
|
148
|
+
json_bytes = base64.b64decode(x_payment_header)
|
|
149
|
+
json_str = json_bytes.decode("utf-8")
|
|
150
|
+
|
|
151
|
+
# Parse JSON
|
|
152
|
+
data = json.loads(json_str)
|
|
153
|
+
|
|
154
|
+
# Validate and parse with Pydantic
|
|
155
|
+
payload = PaymentPayload(**data)
|
|
156
|
+
|
|
157
|
+
logger.debug(f"Extracted payload for network: {payload.network}")
|
|
158
|
+
return payload
|
|
159
|
+
|
|
160
|
+
except base64.binascii.Error as e:
|
|
161
|
+
raise InvalidPayloadError(f"Invalid base64 encoding: {e}")
|
|
162
|
+
except json.JSONDecodeError as e:
|
|
163
|
+
raise InvalidPayloadError(f"Invalid JSON in payload: {e}")
|
|
164
|
+
except Exception as e:
|
|
165
|
+
raise InvalidPayloadError(f"Failed to parse payload: {e}")
|
|
166
|
+
|
|
167
|
+
# =========================================================================
|
|
168
|
+
# Network Validation
|
|
169
|
+
# =========================================================================
|
|
170
|
+
|
|
171
|
+
def validate_network(self, network: str) -> str:
|
|
172
|
+
"""
|
|
173
|
+
Validate that a network is supported and enabled.
|
|
174
|
+
|
|
175
|
+
Handles both v1 ("base") and v2 CAIP-2 ("eip155:8453") formats.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
network: Network identifier (v1 or CAIP-2)
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
Normalized network name
|
|
182
|
+
|
|
183
|
+
Raises:
|
|
184
|
+
UnsupportedNetworkError: If network is not supported
|
|
185
|
+
"""
|
|
186
|
+
# Normalize CAIP-2 to network name
|
|
187
|
+
try:
|
|
188
|
+
normalized = normalize_network(network)
|
|
189
|
+
except ValueError:
|
|
190
|
+
raise UnsupportedNetworkError(
|
|
191
|
+
network=network,
|
|
192
|
+
supported_networks=get_supported_network_names(),
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
network_config = get_network(normalized)
|
|
196
|
+
if not network_config:
|
|
197
|
+
raise UnsupportedNetworkError(
|
|
198
|
+
network=network,
|
|
199
|
+
supported_networks=get_supported_network_names(),
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
if not network_config.enabled:
|
|
203
|
+
raise UnsupportedNetworkError(
|
|
204
|
+
network=network,
|
|
205
|
+
supported_networks=[n for n in get_supported_network_names()
|
|
206
|
+
if get_network(n) and get_network(n).enabled],
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
if not self.config.is_network_enabled(normalized):
|
|
210
|
+
raise UnsupportedNetworkError(
|
|
211
|
+
network=network,
|
|
212
|
+
supported_networks=self.config.supported_networks,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
return normalized
|
|
216
|
+
|
|
217
|
+
# =========================================================================
|
|
218
|
+
# Payment Requirements Building
|
|
219
|
+
# =========================================================================
|
|
220
|
+
|
|
221
|
+
def _build_payment_requirements(
|
|
222
|
+
self,
|
|
223
|
+
payload: PaymentPayload,
|
|
224
|
+
expected_amount_usd: Decimal,
|
|
225
|
+
) -> PaymentRequirements:
|
|
226
|
+
"""
|
|
227
|
+
Build payment requirements for facilitator request.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
payload: Parsed payment payload
|
|
231
|
+
expected_amount_usd: Expected payment amount in USD
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
PaymentRequirements object
|
|
235
|
+
"""
|
|
236
|
+
# Normalize network name (handles CAIP-2 format)
|
|
237
|
+
normalized_network = payload.get_normalized_network()
|
|
238
|
+
|
|
239
|
+
network_config = get_network(normalized_network)
|
|
240
|
+
if not network_config:
|
|
241
|
+
raise UnsupportedNetworkError(
|
|
242
|
+
network=payload.network,
|
|
243
|
+
supported_networks=get_supported_network_names(),
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
# Convert USD to token amount
|
|
247
|
+
expected_amount_wei = network_config.get_token_amount(float(expected_amount_usd))
|
|
248
|
+
|
|
249
|
+
# Get recipient for this network
|
|
250
|
+
recipient = self.config.get_recipient(normalized_network)
|
|
251
|
+
|
|
252
|
+
# Build base requirements
|
|
253
|
+
# Use original network format (v1 or v2) for facilitator
|
|
254
|
+
requirements = PaymentRequirements(
|
|
255
|
+
scheme="exact",
|
|
256
|
+
network=payload.network, # Preserve original format
|
|
257
|
+
maxAmountRequired=str(expected_amount_wei),
|
|
258
|
+
resource=self.config.resource_url or f"https://api.example.com/payment",
|
|
259
|
+
description=self.config.description,
|
|
260
|
+
mimeType="application/json",
|
|
261
|
+
payTo=recipient,
|
|
262
|
+
maxTimeoutSeconds=60,
|
|
263
|
+
asset=network_config.usdc_address,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
# Add EIP-712 domain params for EVM chains
|
|
267
|
+
if network_config.network_type == NetworkType.EVM:
|
|
268
|
+
requirements.extra = {
|
|
269
|
+
"name": network_config.usdc_domain_name,
|
|
270
|
+
"version": network_config.usdc_domain_version,
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return requirements
|
|
274
|
+
|
|
275
|
+
# =========================================================================
|
|
276
|
+
# Facilitator Communication
|
|
277
|
+
# =========================================================================
|
|
278
|
+
|
|
279
|
+
def verify_payment(
|
|
280
|
+
self,
|
|
281
|
+
payload: PaymentPayload,
|
|
282
|
+
expected_amount_usd: Decimal,
|
|
283
|
+
) -> VerifyResponse:
|
|
284
|
+
"""
|
|
285
|
+
Verify payment with the facilitator.
|
|
286
|
+
|
|
287
|
+
This validates the signature/authorization without settling on-chain.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
payload: Parsed payment payload
|
|
291
|
+
expected_amount_usd: Expected payment amount in USD
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
VerifyResponse from facilitator
|
|
295
|
+
|
|
296
|
+
Raises:
|
|
297
|
+
PaymentVerificationError: If verification fails
|
|
298
|
+
FacilitatorError: If facilitator returns an error
|
|
299
|
+
TimeoutError: If request times out
|
|
300
|
+
"""
|
|
301
|
+
normalized_network = self.validate_network(payload.network)
|
|
302
|
+
requirements = self._build_payment_requirements(payload, expected_amount_usd)
|
|
303
|
+
|
|
304
|
+
verify_request = {
|
|
305
|
+
"x402Version": 1,
|
|
306
|
+
"paymentPayload": payload.model_dump(by_alias=True),
|
|
307
|
+
"paymentRequirements": requirements.model_dump(by_alias=True, exclude_none=True),
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
logger.info(f"Verifying payment on {payload.network} for ${expected_amount_usd}")
|
|
311
|
+
logger.debug(f"Verify request: {json.dumps(verify_request, indent=2)}")
|
|
312
|
+
|
|
313
|
+
try:
|
|
314
|
+
client = self._get_http_client()
|
|
315
|
+
response = client.post(
|
|
316
|
+
f"{self.config.facilitator_url}/verify",
|
|
317
|
+
json=verify_request,
|
|
318
|
+
headers={"Content-Type": "application/json"},
|
|
319
|
+
timeout=self.config.verify_timeout,
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
if response.status_code != 200:
|
|
323
|
+
raise FacilitatorError(
|
|
324
|
+
message=f"Facilitator verify failed with status {response.status_code}",
|
|
325
|
+
status_code=response.status_code,
|
|
326
|
+
response_body=response.text,
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
data = response.json()
|
|
330
|
+
verify_response = VerifyResponse(**data)
|
|
331
|
+
|
|
332
|
+
if not verify_response.isValid:
|
|
333
|
+
raise PaymentVerificationError(
|
|
334
|
+
message=f"Payment verification failed: {verify_response.message}",
|
|
335
|
+
reason=verify_response.invalidReason,
|
|
336
|
+
errors=verify_response.errors,
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
logger.info(f"Payment verified! Payer: {verify_response.payer}")
|
|
340
|
+
return verify_response
|
|
341
|
+
|
|
342
|
+
except httpx.TimeoutException:
|
|
343
|
+
raise X402TimeoutError(operation="verify", timeout_seconds=self.config.verify_timeout)
|
|
344
|
+
except httpx.RequestError as e:
|
|
345
|
+
raise FacilitatorError(message=f"Facilitator request failed: {e}")
|
|
346
|
+
|
|
347
|
+
def settle_payment(
|
|
348
|
+
self,
|
|
349
|
+
payload: PaymentPayload,
|
|
350
|
+
expected_amount_usd: Decimal,
|
|
351
|
+
) -> SettleResponse:
|
|
352
|
+
"""
|
|
353
|
+
Settle payment on-chain via the facilitator.
|
|
354
|
+
|
|
355
|
+
This executes the actual on-chain transfer.
|
|
356
|
+
|
|
357
|
+
Args:
|
|
358
|
+
payload: Parsed payment payload
|
|
359
|
+
expected_amount_usd: Expected payment amount in USD
|
|
360
|
+
|
|
361
|
+
Returns:
|
|
362
|
+
SettleResponse from facilitator
|
|
363
|
+
|
|
364
|
+
Raises:
|
|
365
|
+
PaymentSettlementError: If settlement fails
|
|
366
|
+
FacilitatorError: If facilitator returns an error
|
|
367
|
+
TimeoutError: If request times out
|
|
368
|
+
"""
|
|
369
|
+
normalized_network = self.validate_network(payload.network)
|
|
370
|
+
requirements = self._build_payment_requirements(payload, expected_amount_usd)
|
|
371
|
+
|
|
372
|
+
settle_request = {
|
|
373
|
+
"x402Version": 1,
|
|
374
|
+
"paymentPayload": payload.model_dump(by_alias=True),
|
|
375
|
+
"paymentRequirements": requirements.model_dump(by_alias=True, exclude_none=True),
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
logger.info(f"Settling payment on {payload.network} for ${expected_amount_usd}")
|
|
379
|
+
logger.debug(f"Settle request: {json.dumps(settle_request, indent=2)}")
|
|
380
|
+
|
|
381
|
+
try:
|
|
382
|
+
client = self._get_http_client()
|
|
383
|
+
response = client.post(
|
|
384
|
+
f"{self.config.facilitator_url}/settle",
|
|
385
|
+
json=settle_request,
|
|
386
|
+
headers={"Content-Type": "application/json"},
|
|
387
|
+
timeout=self.config.settle_timeout,
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
if response.status_code != 200:
|
|
391
|
+
raise FacilitatorError(
|
|
392
|
+
message=f"Facilitator settle failed with status {response.status_code}",
|
|
393
|
+
status_code=response.status_code,
|
|
394
|
+
response_body=response.text,
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
data = response.json()
|
|
398
|
+
settle_response = SettleResponse(**data)
|
|
399
|
+
|
|
400
|
+
if not settle_response.success:
|
|
401
|
+
raise PaymentSettlementError(
|
|
402
|
+
message=f"Payment settlement failed: {settle_response.message}",
|
|
403
|
+
network=payload.network,
|
|
404
|
+
reason=settle_response.message,
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
tx_hash = settle_response.get_transaction_hash()
|
|
408
|
+
logger.info(f"Payment settled! TX: {tx_hash}, Payer: {settle_response.payer}")
|
|
409
|
+
return settle_response
|
|
410
|
+
|
|
411
|
+
except httpx.TimeoutException:
|
|
412
|
+
raise X402TimeoutError(operation="settle", timeout_seconds=self.config.settle_timeout)
|
|
413
|
+
except httpx.RequestError as e:
|
|
414
|
+
raise FacilitatorError(message=f"Facilitator request failed: {e}")
|
|
415
|
+
|
|
416
|
+
# =========================================================================
|
|
417
|
+
# Main Processing Method
|
|
418
|
+
# =========================================================================
|
|
419
|
+
|
|
420
|
+
def process_payment(
|
|
421
|
+
self,
|
|
422
|
+
x_payment_header: str,
|
|
423
|
+
expected_amount_usd: Decimal,
|
|
424
|
+
) -> PaymentResult:
|
|
425
|
+
"""
|
|
426
|
+
Process a complete x402 payment (verify + settle).
|
|
427
|
+
|
|
428
|
+
This is the main method for handling payments. It:
|
|
429
|
+
1. Extracts and validates the payment payload
|
|
430
|
+
2. Verifies the payment signature with the facilitator
|
|
431
|
+
3. Settles the payment on-chain
|
|
432
|
+
4. Returns the payment result
|
|
433
|
+
|
|
434
|
+
Args:
|
|
435
|
+
x_payment_header: X-PAYMENT header value (base64-encoded JSON)
|
|
436
|
+
expected_amount_usd: Expected payment amount in USD
|
|
437
|
+
|
|
438
|
+
Returns:
|
|
439
|
+
PaymentResult with payer address, transaction hash, etc.
|
|
440
|
+
|
|
441
|
+
Raises:
|
|
442
|
+
InvalidPayloadError: If payload is invalid
|
|
443
|
+
UnsupportedNetworkError: If network is not supported
|
|
444
|
+
PaymentVerificationError: If verification fails
|
|
445
|
+
PaymentSettlementError: If settlement fails
|
|
446
|
+
FacilitatorError: If facilitator returns an error
|
|
447
|
+
TimeoutError: If request times out
|
|
448
|
+
"""
|
|
449
|
+
# Extract payload
|
|
450
|
+
payload = self.extract_payload(x_payment_header)
|
|
451
|
+
logger.info(f"Processing payment: network={payload.network}, amount=${expected_amount_usd}")
|
|
452
|
+
|
|
453
|
+
# Verify payment
|
|
454
|
+
verify_response = self.verify_payment(payload, expected_amount_usd)
|
|
455
|
+
|
|
456
|
+
# Settle payment
|
|
457
|
+
settle_response = self.settle_payment(payload, expected_amount_usd)
|
|
458
|
+
|
|
459
|
+
# Build result
|
|
460
|
+
return PaymentResult(
|
|
461
|
+
success=True,
|
|
462
|
+
payer_address=settle_response.payer or verify_response.payer or "",
|
|
463
|
+
transaction_hash=settle_response.get_transaction_hash(),
|
|
464
|
+
network=payload.network,
|
|
465
|
+
amount_usd=expected_amount_usd,
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
# =========================================================================
|
|
469
|
+
# Convenience Methods
|
|
470
|
+
# =========================================================================
|
|
471
|
+
|
|
472
|
+
def get_payer_address(self, x_payment_header: str) -> Tuple[str, str]:
|
|
473
|
+
"""
|
|
474
|
+
Extract payer address from payment header without processing.
|
|
475
|
+
|
|
476
|
+
Useful for logging or pre-validation.
|
|
477
|
+
|
|
478
|
+
Args:
|
|
479
|
+
x_payment_header: X-PAYMENT header value
|
|
480
|
+
|
|
481
|
+
Returns:
|
|
482
|
+
Tuple of (payer_address, network)
|
|
483
|
+
"""
|
|
484
|
+
payload = self.extract_payload(x_payment_header)
|
|
485
|
+
|
|
486
|
+
# Normalize network name
|
|
487
|
+
normalized_network = payload.get_normalized_network()
|
|
488
|
+
|
|
489
|
+
# Extract payer based on network type
|
|
490
|
+
network_config = get_network(normalized_network)
|
|
491
|
+
if not network_config:
|
|
492
|
+
raise UnsupportedNetworkError(
|
|
493
|
+
network=payload.network,
|
|
494
|
+
supported_networks=get_supported_network_names(),
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
payer = ""
|
|
498
|
+
if network_config.network_type == NetworkType.EVM:
|
|
499
|
+
evm_payload = payload.get_evm_payload()
|
|
500
|
+
payer = evm_payload.authorization.from_address
|
|
501
|
+
elif network_config.network_type == NetworkType.STELLAR:
|
|
502
|
+
stellar_payload = payload.get_stellar_payload()
|
|
503
|
+
payer = stellar_payload.from_address
|
|
504
|
+
# For SVM/NEAR, payer is determined during verification
|
|
505
|
+
|
|
506
|
+
return payer, normalized_network
|
|
507
|
+
|
|
508
|
+
def verify_only(
|
|
509
|
+
self,
|
|
510
|
+
x_payment_header: str,
|
|
511
|
+
expected_amount_usd: Decimal,
|
|
512
|
+
) -> Tuple[bool, str]:
|
|
513
|
+
"""
|
|
514
|
+
Verify payment without settling.
|
|
515
|
+
|
|
516
|
+
Useful for checking payment validity before committing to settlement.
|
|
517
|
+
|
|
518
|
+
Args:
|
|
519
|
+
x_payment_header: X-PAYMENT header value
|
|
520
|
+
expected_amount_usd: Expected payment amount
|
|
521
|
+
|
|
522
|
+
Returns:
|
|
523
|
+
Tuple of (is_valid, payer_address)
|
|
524
|
+
"""
|
|
525
|
+
payload = self.extract_payload(x_payment_header)
|
|
526
|
+
verify_response = self.verify_payment(payload, expected_amount_usd)
|
|
527
|
+
return verify_response.isValid, verify_response.payer or ""
|