Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion python/legacy/src/x402/evm_paywall_template.py

Large diffs are not rendered by default.

22 changes: 16 additions & 6 deletions python/legacy/src/x402/facilitator.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import inspect
from typing import Callable, Optional
from typing_extensions import (
TypedDict,
Expand Down Expand Up @@ -39,14 +40,23 @@ def __init__(self, config: Optional[FacilitatorConfig] = None):

self.config = {"url": url, "create_headers": config.get("create_headers")}

async def _get_custom_headers(self) -> dict[str, dict[str, str]] | None:
"""Get custom headers, supporting both sync and async create_headers functions."""
if not self.config.get("create_headers"):
return None
result = self.config["create_headers"]()
if inspect.iscoroutine(result):
return await result
return result

async def verify(
self, payment: PaymentPayload, payment_requirements: PaymentRequirements
) -> VerifyResponse:
"""Verify a payment header is valid and a request should be processed"""
headers = {"Content-Type": "application/json"}

if self.config.get("create_headers"):
custom_headers = await self.config["create_headers"]()
custom_headers = await self._get_custom_headers()
if custom_headers:
headers.update(custom_headers.get("verify", {}))

async with httpx.AsyncClient() as client:
Expand All @@ -71,8 +81,8 @@ async def settle(
) -> SettleResponse:
headers = {"Content-Type": "application/json"}

if self.config.get("create_headers"):
custom_headers = await self.config["create_headers"]()
custom_headers = await self._get_custom_headers()
if custom_headers:
headers.update(custom_headers.get("settle", {}))

async with httpx.AsyncClient() as client:
Expand Down Expand Up @@ -107,8 +117,8 @@ async def list(

headers = {"Content-Type": "application/json"}

if self.config.get("create_headers"):
custom_headers = await self.config["create_headers"]()
custom_headers = await self._get_custom_headers()
if custom_headers:
headers.update(custom_headers.get("list", {}))

# Build query parameters, excluding None values
Expand Down
2 changes: 1 addition & 1 deletion python/legacy/src/x402/svm_paywall_template.py

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions python/x402/http/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from .facilitator_client import (
AuthHeaders,
AuthProvider,
CreateHeadersAuthProvider,
FacilitatorClient,
FacilitatorConfig,
HTTPFacilitatorClient,
Expand Down Expand Up @@ -84,6 +85,7 @@
"FacilitatorConfig",
"AuthProvider",
"AuthHeaders",
"CreateHeadersAuthProvider",
# HTTP client
"x402HTTPClient",
"PaymentRoundTripper",
Expand Down
58 changes: 48 additions & 10 deletions python/x402/http/facilitator_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

import json
from collections.abc import Callable
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any, Protocol

Expand Down Expand Up @@ -42,6 +43,26 @@ def get_auth_headers(self) -> AuthHeaders:
...


class CreateHeadersAuthProvider:
"""AuthProvider that wraps a create_headers callable.

Adapts the dict-style create_headers function (as used by CDP SDK)
to the AuthProvider protocol.
"""

def __init__(self, create_headers: Callable[[], dict[str, dict[str, str]]]) -> None:
self._create_headers = create_headers

def get_auth_headers(self) -> AuthHeaders:
"""Get authentication headers by calling the create_headers function."""
result = self._create_headers()
return AuthHeaders(
verify=result.get("verify", {}),
settle=result.get("settle", {}),
supported=result.get("supported", result.get("list", {})),
)


# ============================================================================
# FacilitatorClient Protocol
# ============================================================================
Expand Down Expand Up @@ -103,20 +124,37 @@ class HTTPFacilitatorClient:
Supports both V1 and V2 protocol versions.
"""

def __init__(self, config: FacilitatorConfig | None = None) -> None:
def __init__(self, config: FacilitatorConfig | dict[str, Any] | None = None) -> None:
"""Create HTTP facilitator client.

Args:
config: Optional configuration (uses defaults if not provided).
config: Optional configuration. Accepts either:
- FacilitatorConfig dataclass (recommended)
- Dict with 'url' and optional 'create_headers'
- None (uses defaults)
"""
config = config or FacilitatorConfig()

self._url = config.url.rstrip("/")
self._timeout = config.timeout
self._auth_provider = config.auth_provider
self._identifier = config.identifier or self._url
self._http_client = config.http_client
self._owns_client = config.http_client is None
# Handle dict-style config
if isinstance(config, dict):
url = config.get("url", DEFAULT_FACILITATOR_URL)
create_headers = config.get("create_headers")
auth_provider = CreateHeadersAuthProvider(create_headers) if create_headers else None

self._url = url.rstrip("/")
self._timeout = 30.0
self._auth_provider = auth_provider
self._identifier = self._url
self._http_client = None
self._owns_client = True
else:
# Handle FacilitatorConfig dataclass or None
config = config or FacilitatorConfig()

self._url = config.url.rstrip("/")
self._timeout = config.timeout
self._auth_provider = config.auth_provider
self._identifier = config.identifier or self._url
self._http_client = config.http_client
self._owns_client = config.http_client is None

def _get_client(self) -> httpx.Client:
"""Get or create HTTP client."""
Expand Down
2 changes: 0 additions & 2 deletions python/x402/http/x402_http_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -1063,5 +1063,3 @@ def _resolve_value_sync(
)
return result
return value


4 changes: 1 addition & 3 deletions python/x402/tests/integrations/test_async_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,7 @@ def get_body(self):

class TestAsyncHooks:
def setup_method(self):
self.facilitator = x402Facilitator().register(
["x402:cash"], CashSchemeNetworkFacilitator()
)
self.facilitator = x402Facilitator().register(["x402:cash"], CashSchemeNetworkFacilitator())
facilitator_client = CashFacilitatorClient(self.facilitator)
self.resource_server = x402ResourceServer(facilitator_client)
self.resource_server.register("x402:cash", CashSchemeNetworkServer())
Expand Down
8 changes: 2 additions & 6 deletions python/x402/tests/integrations/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -430,9 +430,7 @@ def test_middleware_verify_and_settle_cash_payment(self) -> None:
result.response.body,
)
payment_payload = self.http_client.create_payment_payload(payment_required)
request_headers = self.http_client.encode_payment_signature_header(
payment_payload
)
request_headers = self.http_client.encode_payment_signature_header(payment_payload)

# Retry with payment
mock_adapter_with_payment = MockHTTPAdapter(
Expand All @@ -446,9 +444,7 @@ def test_middleware_verify_and_settle_cash_payment(self) -> None:
method="GET",
)

result2 = asyncio.run(
self.http_server.process_http_request(context_with_payment)
)
result2 = asyncio.run(self.http_server.process_http_request(context_with_payment))
assert result2.type == "payment-verified"
assert result2.payment_payload is not None
assert result2.payment_requirements is not None
Expand Down