from __future__ import annotations
from types import TracebackType
from typing import Literal
import httpx
from .exceptions import IntentsAPIError, IntentsAuthError
from .models import (
AnyInputWithdrawalsResponse,
ExecutionStatusResponse,
QuoteRequest,
QuoteResponse,
SubmitDepositRequest,
SubmitDepositResponse,
TokenResponse,
)
_BASE_URL = "https://1click.chaindefuser.com"
[docs]
class IntentsClient:
"""Async client for the NEAR Intents 1Click Swap API.
Can be used as an async context manager or standalone. When used standalone,
call :meth:`aclose` when finished.
Args:
jwt_token: Optional Bearer JWT token for authenticated endpoints.
base_url: Override the default API base URL.
timeout: Request timeout in seconds (default: 30).
"""
def __init__(
self,
jwt_token: str | None = None,
base_url: str = _BASE_URL,
timeout: float = 30.0,
) -> None:
headers: dict[str, str] = {"Accept": "application/json", "Content-Type": "application/json"}
if jwt_token:
headers["Authorization"] = f"Bearer {jwt_token}"
self._http = httpx.AsyncClient(
base_url=base_url,
headers=headers,
timeout=timeout,
)
async def __aenter__(self) -> IntentsClient:
return self
async def __aexit__(
self,
exc_type: type[BaseException] | None,
exc_val: BaseException | None,
exc_tb: TracebackType | None,
) -> None:
await self.aclose()
[docs]
async def aclose(self) -> None:
await self._http.aclose()
async def _raise_for_status(self, response: httpx.Response) -> None:
if response.status_code == 401:
raise IntentsAuthError(401, "Unauthorized — JWT token is missing or invalid")
if response.is_error:
try:
message = self._parse_json(response).get("message", response.text)
except Exception:
message = response.text
raise IntentsAPIError(response.status_code, message)
def _parse_json(self, response: httpx.Response) -> object:
if not response.content:
raise IntentsAPIError(response.status_code, "Empty response body")
return response.json()
[docs]
async def get_tokens(self) -> list[TokenResponse]:
"""Retrieve all tokens supported by the 1Click API.
Returns:
List of :class:`~intents.models.TokenResponse` objects.
"""
response = await self._http.get("/v0/tokens")
await self._raise_for_status(response)
return [TokenResponse.model_validate(item) for item in self._parse_json(response)] # type: ignore[union-attr]
[docs]
async def get_quote(self, request: QuoteRequest) -> QuoteResponse:
"""Request a swap quote.
Pass ``dry=True`` on the request to simulate the quote without
generating a deposit address.
Args:
request: Swap parameters as a :class:`~intents.models.QuoteRequest`.
Returns:
:class:`~intents.models.QuoteResponse` with the deposit address
and expected output amounts.
Raises:
IntentsAPIError: On HTTP 400 (bad request).
IntentsAuthError: On HTTP 401 (invalid JWT).
"""
response = await self._http.post(
"/v0/quote",
json=request.to_api_dict(),
)
await self._raise_for_status(response)
return QuoteResponse.model_validate(self._parse_json(response))
[docs]
async def get_status(
self,
deposit_address: str,
deposit_memo: str | None = None,
) -> ExecutionStatusResponse:
"""Check the execution status of a swap.
Args:
deposit_address: The deposit address returned by :meth:`get_quote`.
deposit_memo: Required for chains that use memo-based deposits.
Returns:
:class:`~intents.models.ExecutionStatusResponse`.
Raises:
IntentsAPIError: On HTTP 404 (deposit address not found).
IntentsAuthError: On HTTP 401 (invalid JWT).
"""
params: dict[str, str] = {"depositAddress": deposit_address}
if deposit_memo is not None:
params["depositMemo"] = deposit_memo
response = await self._http.get(
"/v0/status",
params=params,
)
await self._raise_for_status(response)
return ExecutionStatusResponse.model_validate(self._parse_json(response))
[docs]
async def submit_deposit(self, request: SubmitDepositRequest) -> SubmitDepositResponse:
"""Notify the 1Click service that a deposit transaction has been sent.
This is optional but can speed up swap processing by allowing the
service to preemptively verify the deposit.
Args:
request: Deposit details as a :class:`~intents.models.SubmitDepositRequest`.
Returns:
:class:`~intents.models.SubmitDepositResponse`.
Raises:
IntentsAPIError: On HTTP 400 (bad request).
IntentsAuthError: On HTTP 401 (invalid JWT).
"""
response = await self._http.post(
"/v0/deposit/submit",
json=request.to_api_dict(),
)
await self._raise_for_status(response)
return SubmitDepositResponse.model_validate(self._parse_json(response))