Skip to content
Open
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
73 changes: 36 additions & 37 deletions src/opengradient/client/x402_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,20 @@
from x402.clients.base import x402Client
from x402.types import PaymentRequirements, x402PaymentRequiredResponse

# Псевдоним типов для чистоты и прохождения проверок
PaymentSelector = typing.Callable[
[
list[PaymentRequirements],
typing.Optional[str],
typing.Optional[str],
typing.Optional[int],
],
PaymentRequirements,
]

class X402Auth(httpx.Auth):
"""
httpx Auth handler for x402 payment protocol.

This class implements the httpx Auth interface to handle 402 Payment Required
responses by automatically creating and attaching payment headers.

Example:
async with httpx.AsyncClient(auth=X402Auth(account=wallet_account)) as client:
response = await client.get("https://api.example.com/paid-resource")
"""

requires_response_body = True
Expand All @@ -31,45 +34,31 @@ def __init__(
self,
account: typing.Any,
max_value: typing.Optional[int] = None,
payment_requirements_selector: typing.Optional[
typing.Callable[
[
list[PaymentRequirements],
typing.Optional[str],
typing.Optional[str],
typing.Optional[int],
],
PaymentRequirements,
]
] = None,
payment_requirements_selector: typing.Optional[PaymentSelector] = None,
network_filter: typing.Optional[str] = None,
scheme_filter: typing.Optional[str] = None,
):
"""
Initialize X402Auth with an Ethereum account for signing payments.

Args:
account: eth_account LocalAccount instance for signing payments
max_value: Optional maximum allowed payment amount in base units
network_filter: Optional network filter for selecting payment requirements
scheme_filter: Optional scheme filter for selecting payment requirements
account: eth_account LocalAccount instance
max_value: Optional maximum allowed payment
network_filter: Optional network filter
scheme_filter: Optional scheme filter
"""
self.x402_client = x402Client(
account,
max_value=max_value,
payment_requirements_selector=payment_requirements_selector, # type: ignore
payment_requirements_selector=payment_requirements_selector,
)
self.network_filter = network_filter

async def async_auth_flow(self, request: httpx.Request) -> typing.AsyncGenerator[httpx.Request, httpx.Response]:
"""
Handle authentication flow for x402 payment protocol.

Args:
request: httpx Request object to be authenticated

Yields:
httpx Request object with authentication headers attached
"""
self.scheme_filter = scheme_filter

async def async_auth_flow(
self, request: httpx.Request
) -> typing.AsyncGenerator[httpx.Request, httpx.Response]:
# Подготавливаем тело запроса для возможности повторной отправки
request.read()

response = yield request

if response.status_code == 402:
Expand All @@ -79,15 +68,25 @@ async def async_auth_flow(self, request: httpx.Request) -> typing.AsyncGenerator

payment_response = x402PaymentRequiredResponse(**data)

# Используем оба фильтра
selected_requirements = self.x402_client.select_payment_requirements(
payment_response.accepts,
self.network_filter,
self.scheme_filter,
)

payment_header = self.x402_client.create_payment_header(selected_requirements, payment_response.x402_version)
if not selected_requirements:
logging.error("X402Auth: No compatible payment requirements found")
return

payment_header = self.x402_client.create_payment_header(
selected_requirements,
payment_response.x402_version
)

request.headers["X-Payment"] = payment_header
request.headers["Access-Control-Expose-Headers"] = "X-Payment-Response"

yield request

except Exception as e:
Expand Down