Skip to content
Open
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
115 changes: 115 additions & 0 deletions extended/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
"""
Extended Exchange SDK - Hyperliquid-compatible interface.

This SDK provides a Hyperliquid/Pacifica-compatible interface for Extended Exchange,
allowing seamless integration with existing trading engines.

Usage:
# Using Client class
from extended import Client

client = Client(
api_key="your-api-key",
vault=12345,
stark_private_key="0x...",
stark_public_key="0x...",
testnet=True,
)

# Info operations
state = client.info.user_state()
orders = client.info.open_orders()

# Exchange operations
client.exchange.order("BTC", is_buy=True, sz=0.01, limit_px=50000)
client.exchange.cancel("BTC", oid=12345)

# Using setup() function (Hyperliquid-style)
from extended import setup

address, info, exchange = setup(
api_key="your-api-key",
vault=12345,
stark_private_key="0x...",
stark_public_key="0x...",
testnet=True
)

state = info.user_state()
exchange.order("BTC", is_buy=True, sz=0.01, limit_px=50000)

Note:
Credentials (api_key, vault, stark keys) must be obtained from your
onboarding infrastructure. This SDK does not perform onboarding.

Key Differences from Hyperliquid:
- Credentials: Extended requires 4 credentials from external onboarding
- No address param: Can't query other users (Extended requires auth)
- Market names: Extended uses "BTC-USD" internally (auto-converted from "BTC")
- Bulk orders: Not atomic on Extended
- Isolated margin: Not supported on Extended
- Market orders: Simulated as IOC limit orders (may not fill completely)
"""

__version__ = "0.1.0"

# Main clients - NATIVE SYNC ONLY
from extended.client import Client

# Setup function (Hyperliquid-style) - NATIVE SYNC ONLY
from extended.setup import setup

# API classes (for type hints) - NATIVE SYNC ONLY
from extended.api import InfoAPI, ExchangeAPI

# Configuration
from extended.config_sync import TESTNET_CONFIG, MAINNET_CONFIG, SimpleSyncConfig as EndpointConfig

# Exceptions
from extended.exceptions_sync import (
ExtendedError,
ExtendedAPIError,
ExtendedAuthError,
ExtendedRateLimitError,
ExtendedValidationError,
ExtendedNotFoundError,
)

# Simple types to avoid dependencies
class Side:
BUY = "BUY"
SELL = "SELL"

class TimeInForce:
GTC = "GTC"
IOC = "IOC"
ALO = "ALO"

OrderTypeSpec = dict
BuilderInfo = dict

__all__ = [
# Version
"__version__",
# Main clients - NATIVE SYNC ONLY
"Client",
# API classes - NATIVE SYNC ONLY
"InfoAPI",
"ExchangeAPI",
# Config
"TESTNET_CONFIG",
"MAINNET_CONFIG",
"EndpointConfig",
# Exceptions
"ExtendedError",
"ExtendedAPIError",
"ExtendedAuthError",
"ExtendedRateLimitError",
"ExtendedValidationError",
"ExtendedNotFoundError",
# Types
"Side",
"TimeInForce",
"OrderTypeSpec",
"BuilderInfo",
]
12 changes: 12 additions & 0 deletions extended/api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"""
API modules for Extended Exchange SDK.
"""

# Only import sync APIs - async imports removed to eliminate dependencies
from extended.api.info import InfoAPI
from extended.api.exchange import ExchangeAPI

__all__ = [
"InfoAPI",
"ExchangeAPI",
]
39 changes: 39 additions & 0 deletions extended/api/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""
Base sync API class for Extended Exchange SDK.

Provides common functionality for sync API classes using native sync implementation.
Uses native sync implementation instead of wrapper approach.
"""

from typing import Any, TypeVar

from extended.api.base_native_sync import BaseNativeSyncClient
from extended.auth_sync import SimpleSyncAuth
from extended.config_sync import SimpleSyncConfig

T = TypeVar("T")


class BaseSyncAPI(BaseNativeSyncClient):
"""
Base class for sync API implementations.

Uses native sync HTTP operations instead of wrapping async operations.
MIRRORS Pacifica's BaseAPIClient approach exactly.
"""

def __init__(self, auth: SimpleSyncAuth, config: SimpleSyncConfig):
"""
Initialize the base API.

Args:
auth: SimpleSyncAuth instance with credentials
config: SimpleSyncConfig configuration
"""
# Use native sync client directly
super().__init__(auth, config)

def close(self):
"""Close the API and release resources."""
# No async cleanup needed for native sync implementation
pass
63 changes: 63 additions & 0 deletions extended/api/base_async.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""
Base async API class for Extended Exchange SDK.

Provides common functionality for async API classes.
"""

from typing import Any, Awaitable, Callable, List, TypeVar

from x10.perpetual.configuration import EndpointConfig
from x10.perpetual.trading_client import PerpetualTradingClient

from extended.auth import ExtendedAuth
from extended.utils.async_helpers import thread_safe_gather

T = TypeVar("T")


class BaseAsyncAPI:
"""
Base class for async API implementations.

Provides access to the Extended trading client and common utilities.
"""

def __init__(self, auth: ExtendedAuth, config: EndpointConfig):
"""
Initialize the base API.

Args:
auth: ExtendedAuth instance with credentials
config: Endpoint configuration
"""
self._auth = auth
self._config = config
self._client: PerpetualTradingClient = auth.get_trading_client()

@property
def trading_client(self) -> PerpetualTradingClient:
"""Get the underlying trading client."""
return self._client

async def execute_parallel(
self,
tasks: List[Callable[[], Awaitable[T]]],
) -> List[T]:
"""
Execute multiple async tasks in parallel.

Uses thread-safe gather to prevent "Future attached to
different loop" errors in ThreadPoolExecutor contexts.

Args:
tasks: List of async callables

Returns:
List of results from all tasks
"""
coroutines = [task() for task in tasks]
return await thread_safe_gather(*coroutines)

async def close(self):
"""Close the API and release resources."""
await self._auth.close()
156 changes: 156 additions & 0 deletions extended/api/base_native_sync.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
"""
Base native sync API client for Extended Exchange SDK.

MIRRORS Pacifica's BaseAPIClient architecture exactly.
Uses requests.Session() for native synchronous operation.
"""

import requests
import time
import logging
from typing import Dict, Optional, Any, List
from urllib.parse import urljoin

from extended.auth_sync import SimpleSyncAuth
from extended.config_sync import SimpleSyncConfig

# Simple exception class to avoid async dependencies
class ExtendedAPIError(Exception):
def __init__(self, status_code: int, message: str, data=None):
self.status_code = status_code
self.message = message
self.data = data
super().__init__(message)

logger = logging.getLogger(__name__)


class BaseNativeSyncClient:
"""
Base native sync HTTP client - EXACT MIRROR of Pacifica's BaseAPIClient.

Uses requests.Session() for native sync HTTP operations,
eliminating all async/event loop dependencies.
"""

def __init__(
self,
auth: Optional[SimpleSyncAuth] = None,
config: Optional[SimpleSyncConfig] = None,
timeout: int = 30
):
"""
Initialize base API client.

Args:
auth: SimpleSyncAuth instance with credentials
config: SimpleSyncConfig configuration
timeout: Request timeout in seconds
"""
self.auth = auth
self.config = config
self.timeout = timeout

# Native sync session - SAME AS PACIFICA
self.session = requests.Session()
self.session.headers.update({
"Content-Type": "application/json",
"Accept": "application/json"
})

def _request(
self,
method: str,
endpoint: str,
params: Optional[Dict] = None,
data: Optional[Dict] = None,
authenticated: bool = False,
additional_headers: Optional[Dict] = None
) -> Dict[str, Any]:
"""
Make an API request - EXACT COPY of Pacifica's _request method.

Args:
method: HTTP method
endpoint: API endpoint
params: Query parameters
data: Request body
authenticated: Whether request needs authentication
additional_headers: Additional headers

Returns:
API response data

Raises:
ExtendedAPIError: On API errors
"""
# Fix URL construction: ensure proper path joining
if endpoint.startswith('/'):
url = self.config.api_base_url + endpoint
else:
url = f"{self.config.api_base_url}/{endpoint}"

headers = {}
if authenticated and self.auth:
headers["X-Api-Key"] = self.auth.api_key

if additional_headers:
headers.update(additional_headers)

logger.debug(f"{method} {url} params={params}")

try:
response = self.session.request(
method,
url,
params=params,
json=data,
headers=headers,
timeout=self.timeout
)

# Handle HTTP errors
if response.status_code >= 400:
try:
error_data = response.json()
message = error_data.get("msg", response.text)
except:
message = response.text
raise ExtendedAPIError(response.status_code, message)

result = response.json()

# Check for API-level errors in response
if not result.get("success", True):
raise ExtendedAPIError(
response.status_code,
result.get("msg", "Request failed"),
result
)

return result

except requests.RequestException as e:
logger.error(f"Request failed: {e}")
raise ExtendedAPIError(500, str(e))

def get(self, endpoint: str, params: Optional[Dict] = None, authenticated: bool = False) -> Dict:
"""GET request"""
return self._request("GET", endpoint, params=params, authenticated=authenticated)

def post(self, endpoint: str, data: Optional[Dict] = None, authenticated: bool = True, headers: Optional[Dict] = None) -> Dict:
"""POST request with optional headers"""
return self._request("POST", endpoint, data=data, authenticated=authenticated, additional_headers=headers)

def delete(self, endpoint: str, params: Optional[Dict] = None, authenticated: bool = True) -> Dict:
"""DELETE request"""
return self._request("DELETE", endpoint, params=params, authenticated=authenticated)

def patch(self, endpoint: str, data: Optional[Dict] = None, authenticated: bool = True) -> Dict:
"""PATCH request"""
return self._request("PATCH", endpoint, data=data, authenticated=authenticated)

def close(self):
"""Close the session."""
if self.session:
self.session.close()
Loading