From 8be5e032323bf4d3887cfc94682b66303152b7f4 Mon Sep 17 00:00:00 2001 From: zarathustra Date: Thu, 30 Oct 2025 20:09:09 +0100 Subject: [PATCH 01/22] chore: remove old endpoints.py --- derive_client/endpoints.py | 70 -------------------------------------- 1 file changed, 70 deletions(-) delete mode 100644 derive_client/endpoints.py diff --git a/derive_client/endpoints.py b/derive_client/endpoints.py deleted file mode 100644 index c0074b90..00000000 --- a/derive_client/endpoints.py +++ /dev/null @@ -1,70 +0,0 @@ -from typing import Any - - -class Endpoint: - """Descriptor that formats a full URL from the client's base_url, section, and path.""" - - def __init__(self, section: str, path: str): - self.section = section.strip("/") - self.path = path.strip("/") - - def __get__(self, inst: Any, owner: Any) -> str: - if inst is None: - return self # allow access on the class for introspection - base = inst._base_url.rstrip("/") - return f"{base}/{self.section}/{self.path}" - - -class PublicEndpoints: - def __init__(self, base_url: str): - self._base_url = base_url - - create_account = Endpoint("public", "create_account") - get_instruments = Endpoint("public", "get_instruments") - get_ticker = Endpoint("public", "get_ticker") - get_all_currencies = Endpoint("public", "get_all_currencies") - get_currency = Endpoint("public", "get_currency") - get_transaction = Endpoint("public", "get_transaction") - - -class PrivateEndpoints: - def __init__(self, base_url: str): - self._base_url = base_url - - session_keys = Endpoint("private", "session_keys") - get_subaccount = Endpoint("private", "get_subaccount") - get_subaccounts = Endpoint("private", "get_subaccounts") - get_order = Endpoint("private", "get_order") - get_orders = Endpoint("private", "get_orders") - get_positions = Endpoint("private", "get_positions") - get_collaterals = Endpoint("private", "get_collaterals") - create_subaccount = Endpoint("private", "create_subaccount") - transfer_erc20 = Endpoint("private", "transfer_erc20") - transfer_position = Endpoint("private", "transfer_position") - transfer_positions = Endpoint("private", "transfer_positions") - get_mmp_config = Endpoint("private", "get_mmp_config") - set_mmp_config = Endpoint("private", "set_mmp_config") - send_rfq = Endpoint("private", "send_rfq") - poll_rfqs = Endpoint("private", "poll_rfqs") - poll_quotes = Endpoint("private", "poll_quotes") - cancel_rfq = Endpoint("private", "cancel_rfq") - cancel_batch_rfqs = Endpoint("private", "cancel_batch_rfqs") - execute_quote = Endpoint("private", "execute_quote") - send_quote = Endpoint("private", "send_quote") - poll_quotes = Endpoint("private", "poll_quotes") - execute_quote = Endpoint("private", "execute_quote") - deposit = Endpoint("private", "deposit") - withdraw = Endpoint("private", "withdraw") - order = Endpoint("private", "order") - cancel = Endpoint("private", "cancel") - cancel_all = Endpoint("private", "cancel_all") - - -class RestAPI: - public: PublicEndpoints - private: PrivateEndpoints - - def __init__(self, base_url: str): - self._base_url = base_url - self.public = PublicEndpoints(base_url) - self.private = PrivateEndpoints(base_url) From 3438389a4a7cb032ca7b4f0507356c19c3aa6106 Mon Sep 17 00:00:00 2001 From: zarathustra Date: Thu, 30 Oct 2025 20:09:31 +0100 Subject: [PATCH 02/22] chore: remove old clients module --- derive_client/clients/__init__.py | 13 - derive_client/clients/async_client.py | 535 ------------ derive_client/clients/base_client.py | 1104 ------------------------- derive_client/clients/http_client.py | 107 --- derive_client/clients/ws_client.py | 440 ---------- derive_client/derive.py | 26 - 6 files changed, 2225 deletions(-) delete mode 100644 derive_client/clients/__init__.py delete mode 100644 derive_client/clients/async_client.py delete mode 100644 derive_client/clients/base_client.py delete mode 100644 derive_client/clients/http_client.py delete mode 100644 derive_client/clients/ws_client.py delete mode 100644 derive_client/derive.py diff --git a/derive_client/clients/__init__.py b/derive_client/clients/__init__.py deleted file mode 100644 index ce8f0fd0..00000000 --- a/derive_client/clients/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -"""Clients module""" - -from .async_client import AsyncClient -from .base_client import BaseClient -from .http_client import HttpClient -from .ws_client import WsClient - -__all__ = [ - "BaseClient", - "AsyncClient", - "HttpClient", - "WsClient", -] diff --git a/derive_client/clients/async_client.py b/derive_client/clients/async_client.py deleted file mode 100644 index 4d08c33b..00000000 --- a/derive_client/clients/async_client.py +++ /dev/null @@ -1,535 +0,0 @@ -""" -Async client for Derive -""" - -import asyncio -import functools -import json -import time -from datetime import datetime -from decimal import Decimal - -import aiohttp -from derive_action_signing.utils import sign_ws_login, utc_now_ms - -from derive_client._bridge import BridgeClient -from derive_client._bridge.standard_bridge import StandardBridge -from derive_client.constants import DEFAULT_REFERER, TEST_PRIVATE_KEY -from derive_client.data_types import ( - Address, - BridgeTxResult, - ChainID, - Currency, - Environment, - InstrumentType, - OrderSide, - OrderType, - PreparedBridgeTx, - TimeInForce, - UnderlyingCurrency, -) -from derive_client.utils import unwrap_or_raise - -from .base_client import BaseClient, DeriveJSONRPCException - - -class AsyncClient(BaseClient): - """ - We use the async client to make async requests to the derive API - We us the ws client to make async requests to the derive ws API - """ - - current_subscriptions = {} - listener = None - subscribing = False - - def __init__( - self, - private_key: str = TEST_PRIVATE_KEY, - env: Environment = Environment.TEST, - logger=None, - verbose=False, - subaccount_id=None, - wallet=None, - ): - super().__init__( - wallet=wallet, - private_key=private_key, - env=env, - logger=logger, - verbose=verbose, - subaccount_id=subaccount_id, - ) - - self.message_queues = {} - self.connecting = False - - @functools.cached_property - def _bridge(self) -> BridgeClient: - return BridgeClient(env=self.env, account=self.signer, wallet=self.wallet, logger=self.logger) - - @functools.cached_property - def _standard_bridge(self) -> StandardBridge: - return StandardBridge(self.account, self.logger) - - async def prepare_standard_tx( - self, - human_amount: float, - currency: Currency, - to: Address, - source_chain: ChainID, - target_chain: ChainID, - ) -> PreparedBridgeTx: - """ - Prepare a transaction to bridge tokens to using Standard Bridge. - - This creates a signed transaction ready for submission but does not execute it. - Review the returned PreparedBridgeTx before calling submit_bridge_tx(). - - Args: - human_amount: Amount in token units (e.g., 1.5 USDC, 0.1 ETH) - currency: Currency enum value describing the token to bridge - to: Destination address on the target chain - source_chain: ChainID for the source chain - target_chain: ChainID for the target chain - - Returns: - PreparedBridgeTx: Contains transaction details including: - - tx_hash: Pre-computed transaction hash - - nonce: Transaction nonce for replacement/cancellation - - tx_details: Contract address, method, gas estimates, signed transaction - - currency, amount, source_chain, target_chain, bridge_type: Bridge context - - Use the returned object to: - - Verify contract addresses and gas costs before submission - - Submit with submit_bridge_tx() on approval - """ - - result = await self._standard_bridge.prepare_tx( - human_amount=human_amount, - currency=currency, - to=to, - source_chain=source_chain, - target_chain=target_chain, - ) - - return unwrap_or_raise(result) - - async def prepare_deposit_to_derive( - self, - human_amount: float, - currency: Currency, - chain_id: ChainID, - ) -> PreparedBridgeTx: - """ - Prepare a deposit transaction to bridge tokens to Derive. - - This creates a signed transaction ready for submission but does not execute it. - Review the returned PreparedBridgeTx before calling submit_bridge_tx(). - - Args: - human_amount: Amount in token units (e.g., 1.5 USDC, 0.1 ETH) - currency: Token to bridge - chain_id: Source chain to bridge from - - Returns: - PreparedBridgeTx: Contains transaction details including: - - tx_hash: Pre-computed transaction hash - - nonce: Transaction nonce for replacement/cancellation - - tx_details: Contract address, method, gas estimates, signed transaction - - currency, amount, source_chain, target_chain, bridge_type: Bridge context - - Use the returned object to: - - Verify contract addresses and gas costs before submission - - Submit with submit_bridge_tx() on approval - """ - - if currency is Currency.ETH: - raise NotImplementedError( - "ETH deposits to the funding wallet (Light Account) are not implemented. " - "For gas funding of the owner (EOA) use `prepare_standard_tx`." - ) - - result = await self._bridge.prepare_deposit(human_amount=human_amount, currency=currency, chain_id=chain_id) - return unwrap_or_raise(result) - - async def prepare_withdrawal_from_derive( - self, - human_amount: float, - currency: Currency, - chain_id: ChainID, - ) -> PreparedBridgeTx: - """ - Prepare a withdrawal transaction to bridge tokens from Derive. - - This creates a signed transaction ready for submission but does not execute it. - Review the returned PreparedBridgeTx before calling submit_bridge_tx(). - - Args: - human_amount: Amount in token units (e.g., 1.5 USDC, 0.1 ETH) - currency: Token to bridge - chain_id: Target chain to bridge to - - Returns: - PreparedBridgeTx: Contains transaction details including: - - tx_hash: Pre-computed transaction hash - - nonce: Transaction nonce for replacement/cancellation - - tx_details: Contract address, method, gas estimates, signed transaction - - currency, amount, source_chain, target_chain, bridge_type: Bridge context - - Use the returned object to: - - Verify contract addresses and gas costs before submission - - Submit with submit_bridge_tx() when ready - """ - - result = await self._bridge.prepare_withdrawal(human_amount=human_amount, currency=currency, chain_id=chain_id) - return unwrap_or_raise(result) - - async def submit_bridge_tx(self, prepared_tx: PreparedBridgeTx) -> BridgeTxResult: - """ - Submit a prepared bridge transaction to the blockchain. - - This broadcasts the signed transaction and returns tracking information. - The transaction is submitted but not yet confirmed - use poll_bridge_progress() - to monitor completion. - - Args: - prepared_tx: Transaction prepared by prepare_deposit_to_derive() - or prepare_withdrawal_from_derive() - - Returns: - BridgeTxResult: Initial tracking object containing: - - source_tx: Transaction hash on source chain (unconfirmed) - - target_from_block: Block number to start polling target chain events - - tx_details: Copy of original transaction details - - currency, bridge, source_chain, target_chain: Bridge context - - Next steps: - - Call poll_bridge_progress() to wait for cross-chain completion - """ - - if prepared_tx.currency == Currency.ETH: - result = await self._standard_bridge.submit_bridge_tx(prepared_tx=prepared_tx) - else: - result = await self._bridge.submit_bridge_tx(prepared_tx=prepared_tx) - - return unwrap_or_raise(result) - - async def poll_bridge_progress(self, tx_result: BridgeTxResult) -> BridgeTxResult: - """ - Poll for bridge transaction completion across both chains. - - This monitors the full cross-chain bridge pipeline: - 1. Source chain finality - 2. Target chain event detection - 3. Target chain finality - - Args: - tx_result: Result from submit_bridge_tx() or previous poll attempt - - Returns: - BridgeTxResult: Updated with completed bridge information: - - source_tx.tx_receipt: Source chain transaction receipt (confirmed) - - target_tx.tx_hash: Target chain transaction hash - - target_tx.tx_receipt: Target chain transaction receipt (confirmed) - - Raises: - PartialBridgeResult: Pipeline failed at some step. The exception contains - the partially updated tx_result for inspection and retry. Common scenarios: - - FinalityTimeout: Not enough confirmations, wait longer - - TxPendingTimeout: Transaction stuck, consider resubmission - - TransactionDropped: Transaction lost, likely needs resubmission - - Recovery strategies: - - On PartialBridgeResult: inspect the tx_result in the exception - - For FinalityTimeout: call poll_bridge_progress() again with the partial result - - For TransactionDropped: prepare new tx with same nonce to replace - - For TxPendingTimeout: prepare new tx with higher gas using same nonce. - - In case of a nonce collision: verify whether previous transaction got included - or whether the nonce was reused in another tx. - """ - - if tx_result.currency == Currency.ETH: - result = await self._standard_bridge.poll_bridge_progress(tx_result=tx_result) - else: - result = await self._bridge.poll_bridge_progress(tx_result=tx_result) - - return unwrap_or_raise(result) - - def get_subscription_id(self, instrument_name: str, group: str = "1", depth: str = "100"): - return f"orderbook.{instrument_name}.{group}.{depth}" - - async def subscribe(self, instrument_name: str, group: str = "1", depth: str = "100"): - """ - Subscribe to the order book for a symbol - """ - # if self.listener is None or self.listener.done(): - asyncio.create_task(self.listen_for_messages()) - channel = self.get_subscription_id(instrument_name, group, depth) - if channel not in self.message_queues: - self.message_queues[channel] = asyncio.Queue() - msg = {"method": "subscribe", "params": {"channels": [channel]}} - await self.ws.send_json(msg) - return - - while instrument_name not in self.current_subscriptions: - await asyncio.sleep(0.01) - return self.current_subscriptions[instrument_name] - - async def connect_ws(self): - self.connecting = True - self.session = aiohttp.ClientSession() - ws = await self.session.ws_connect(self.config.ws_address) - self._ws = ws - self.connecting = False - return ws - - async def listen_for_messages( - self, - ): - while True: - try: - msg = await self.ws.receive_json() - except TypeError: - continue - if "error" in msg: - raise Exception(msg["error"]) - if "result" in msg: - result = msg["result"] - if "status" in result: - for channel, value in result['status'].items(): - if "error" in value: - raise Exception(f"Subscription error for channel: {channel} error: {value['error']}") - continue - # default to putting the message in the queue - subscription = msg['params']['channel'] - data = msg['params']['data'] - self.handle_message(subscription, data) - - async def login_client( - self, - retries=3, - ): - login_request = { - 'method': 'public/login', - 'params': sign_ws_login( - web3_client=self.web3_client, - smart_contract_wallet=self.wallet, - session_key_or_wallet_private_key=self.signer._private_key, - ), - 'id': str(utc_now_ms()), - } - await self._ws.send_json(login_request) - # we need to wait for the response - async for msg in self._ws: - message = json.loads(msg.data) - if message['id'] == login_request['id']: - if "result" not in message: - if self._check_output_for_rate_limit(message): - return await self.login_client() - raise DeriveJSONRPCException(**message['error']) - break - - def handle_message(self, subscription, data): - bids = data['bids'] - asks = data['asks'] - - bids = list(map(lambda x: (float(x[0]), float(x[1])), bids)) - asks = list(map(lambda x: (float(x[0]), float(x[1])), asks)) - - instrument_name = subscription.split(".")[1] - - if subscription in self.current_subscriptions: - old_params = self.current_subscriptions[subscription] - _asks, _bids = old_params["asks"], old_params["bids"] - if not asks: - asks = _asks - if not bids: - bids = _bids - timestamp = data['timestamp'] - datetime_str = datetime.fromtimestamp(timestamp / 1000) - nonce = data['publish_id'] - self.current_subscriptions[instrument_name] = { - "asks": asks, - "bids": bids, - "timestamp": timestamp, - "datetime": datetime_str.isoformat(), - "nonce": nonce, - "symbol": instrument_name, - } - return self.current_subscriptions[instrument_name] - - async def watch_order_book(self, instrument_name: str, group: str = "1", depth: str = "100"): - """ - Watch the order book for a symbol - orderbook.{instrument_name}.{group}.{depth} - """ - - if not self.ws and not self.connecting: - await self.connect_ws() - await self.login_client() - - subscription = self.get_subscription_id(instrument_name, group, depth) - - if subscription not in self.message_queues: - while any([self.subscribing, self.ws is None, self.connecting]): - await asyncio.sleep(1) - await self.subscribe(instrument_name, group, depth) - - while instrument_name not in self.current_subscriptions and not self.connecting: - await asyncio.sleep(0.01) - - return self.current_subscriptions[instrument_name] - - async def fetch_instruments( - self, - expired=False, - instrument_type: InstrumentType = InstrumentType.PERP, - currency: UnderlyingCurrency = UnderlyingCurrency.BTC, - ): - return super().fetch_instruments(expired, instrument_type, currency) - - async def close(self): - """ - Close the connection - """ - self.ws.close() - - async def fetch_tickers( - self, - instrument_type: InstrumentType = InstrumentType.OPTION, - currency: UnderlyingCurrency = UnderlyingCurrency.BTC, - ): - if not self._ws: - await self.connect_ws() - instruments = await self.fetch_instruments(instrument_type=instrument_type, currency=currency) - instrument_names = [i['instrument_name'] for i in instruments] - id_base = str(int(time.time())) - ids_to_instrument_names = { - f'{id_base}_{enumerate}': instrument_name for enumerate, instrument_name in enumerate(instrument_names) - } - for id, instrument_name in ids_to_instrument_names.items(): - payload = {"instrument_name": instrument_name} - await self._ws.send_json({'method': 'public/get_ticker', 'params': payload, 'id': id}) - await asyncio.sleep(0.1) # otherwise we get rate limited... - results = {} - while ids_to_instrument_names: - message = await self._ws.receive() - if message is None: - continue - if 'error' in message: - raise Exception(f"Error fetching ticker {message}") - if message.type == aiohttp.WSMsgType.CLOSED: - # we try to reconnect - self.logger.error(f"Error fetching ticker {message}...") - self._ws = await self.connect_ws() - return await self.fetch_tickers(instrument_type, currency) - message = json.loads(message.data) - if message['id'] in ids_to_instrument_names: - try: - results[message['result']['instrument_name']] = message['result'] - except KeyError: - self.logger.error(f"Error fetching ticker {message}") - del ids_to_instrument_names[message['id']] - return results - - async def get_collaterals(self): - return super().get_collaterals() - - async def get_positions(self, currency: UnderlyingCurrency = UnderlyingCurrency.BTC): - return super().get_positions() - - async def get_open_orders(self, status, currency: UnderlyingCurrency = UnderlyingCurrency.BTC): - return super().fetch_orders( - status=status, - ) - - async def fetch_ticker(self, instrument_name: str): - """ - Fetch the ticker for a symbol - """ - return super().fetch_ticker(instrument_name) - - async def create_order( - self, - price, - amount, - instrument_name: str, - reduce_only=False, - side: OrderSide = OrderSide.BUY, - order_type: OrderType = OrderType.LIMIT, - time_in_force: TimeInForce = TimeInForce.GTC, - instrument_type: InstrumentType = InstrumentType.PERP, - underlying_currency: UnderlyingCurrency = UnderlyingCurrency.USDC, - ): - """ - Create the order. - """ - if not self._ws: - await self.connect_ws() - await self.login_client() - if side.name.upper() not in OrderSide.__members__: - raise Exception(f"Invalid side {side}") - instruments = await self._internal_map_instrument(instrument_type, underlying_currency) - instrument = instruments[instrument_name] - - rounded_price = Decimal(price).quantize( - Decimal(instrument['tick_size']), - ) - rounded_amount = Decimal(amount).quantize( - Decimal(instrument['amount_step']), - ) - - module_data = { - "asset_address": instrument['base_asset_address'], - "sub_id": int(instrument['base_asset_sub_id']), - "limit_price": rounded_price, - "amount": rounded_amount, - "max_fee": Decimal(1000), - "recipient_id": int(self.subaccount_id), - "is_bid": side == OrderSide.BUY, - } - - signed_action = self._generate_signed_action( - module_address=self.config.contracts.TRADE_MODULE, module_data=module_data - ) - - order = { - "instrument_name": instrument_name, - "direction": side.name.lower(), - "order_type": order_type.name.lower(), - "mmp": False, - "time_in_force": time_in_force.value, - "referral_code": DEFAULT_REFERER, - **signed_action.to_json(), - } - try: - response = await self.submit_order(order) - except aiohttp.ClientConnectionResetError: - await self.connect_ws() - await self.login_client() - response = await self.submit_order(order) - return response - - async def _internal_map_instrument(self, instrument_type, currency): - """ - Map the instrument. - """ - instruments = await self.fetch_instruments(instrument_type=instrument_type, currency=currency) - return {i['instrument_name']: i for i in instruments} - - async def submit_order(self, order): - id = str(utc_now_ms()) - await self._ws.send_json({'method': 'private/order', 'params': order, 'id': id}) - while True: - async for msg in self._ws: - message = json.loads(msg.data) - if message['id'] == id: - try: - if "result" not in message: - if self._check_output_for_rate_limit(message): - return await self.submit_order(order) - raise DeriveJSONRPCException(**message['error']) - return message['result']['order'] - except KeyError as error: - raise Exception(f"Unable to submit order {message}") from error diff --git a/derive_client/clients/base_client.py b/derive_client/clients/base_client.py deleted file mode 100644 index 23302ea9..00000000 --- a/derive_client/clients/base_client.py +++ /dev/null @@ -1,1104 +0,0 @@ -""" -Base Client for the derive dex. -""" - -import json -import random -import time -from decimal import Decimal -from logging import Logger, LoggerAdapter -from time import sleep - -import requests -from derive_action_signing.module_data import ( - DepositModuleData, - MakerTransferPositionModuleData, - MakerTransferPositionsModuleData, - RecipientTransferERC20ModuleData, - RFQQuoteDetails, - RFQQuoteModuleData, - SenderTransferERC20ModuleData, - TakerTransferPositionModuleData, - TakerTransferPositionsModuleData, - TradeModuleData, - TransferERC20Details, - TransferPositionsDetails, - WithdrawModuleData, -) -from derive_action_signing.signed_action import SignedAction -from derive_action_signing.utils import MAX_INT_32, get_action_nonce, sign_rest_auth_header, utc_now_ms -from hexbytes import HexBytes -from pydantic import validate_call -from web3 import Web3 - -from derive_client.constants import CONFIGS, DEFAULT_REFERER, PUBLIC_HEADERS, TOKEN_DECIMALS -from derive_client.data_types import ( - Address, - CollateralAsset, - CreateSubAccountData, - CreateSubAccountDetails, - DepositResult, - DeriveTxResult, - DeriveTxStatus, - Environment, - InstrumentType, - MainnetCurrency, - ManagerAddress, - MarginType, - OrderSide, - OrderStatus, - OrderType, - PositionSpec, - PositionsTransfer, - PositionTransfer, - RfqStatus, - SessionKey, - SubaccountType, - TimeInForce, - UnderlyingCurrency, - WithdrawResult, -) -from derive_client.endpoints import RestAPI -from derive_client.exceptions import DeriveJSONRPCException -from derive_client.utils import get_logger, wait_until - - -def _is_final_tx(res: DeriveTxResult) -> bool: - return res.status not in (DeriveTxStatus.REQUESTED, DeriveTxStatus.PENDING) - - -class BaseClient: - """Client for the Derive dex.""" - - def _create_signature_headers(self): - """ - Create the signature headers. - """ - return sign_rest_auth_header( - web3_client=self.web3_client, - smart_contract_wallet=self.wallet, - session_key_or_wallet_private_key=self.signer._private_key, - ) - - @validate_call(config=dict(arbitrary_types_allowed=True)) - def __init__( - self, - wallet: Address, - private_key: str | HexBytes, - env: Environment, - logger: Logger | LoggerAdapter | None = None, - verbose: bool = False, - subaccount_id: int | None = None, - ): - self.verbose = verbose - self.env = env - self.config = CONFIGS[env] - self.logger = logger or get_logger() - self.web3_client = Web3(Web3.HTTPProvider(self.config.rpc_endpoint)) - self.signer = self.web3_client.eth.account.from_key(private_key) - self.wallet = wallet - self._verify_wallet(wallet) - self.subaccount_ids = self.fetch_subaccounts().get("subaccount_ids", []) - if subaccount_id is not None and subaccount_id not in self.subaccount_ids: - msg = f"Provided subaccount {subaccount_id} not among retrieved aubaccounts: {self.subaccounts!r}" - raise ValueError(msg) - self.subaccount_id = subaccount_id or self.subaccount_ids[0] - - @property - def account(self): - return self.signer - - @property - def private_key(self) -> HexBytes: - return self.account._private_key - - @property - def endpoints(self) -> RestAPI: - """Return the chain ID.""" - return RestAPI(self.config.base_url) - - def _verify_wallet(self, wallet: Address): - if not self.web3_client.is_connected(): - raise ConnectionError(f"Failed to connect to RPC at {self.config.rpc_endpoint}") - if not self.web3_client.eth.get_code(wallet): - msg = f"{wallet} appears to be an EOA (no bytecode). Expected a smart-contract wallet on Derive." - raise ValueError(msg) - session_keys = self._get_session_keys(wallet) - if not any(self.signer.address == s.public_session_key for s in session_keys): - msg = f"{self.signer.address} is not among registered session keys for wallet {wallet}." - raise ValueError(msg) - - def create_account(self, wallet): - """Call the create account endpoint.""" - payload = {"wallet": wallet} - url = self.endpoints.public.create_account - result = requests.post( - headers=PUBLIC_HEADERS, - url=url, - json=payload, - ) - result_code = json.loads(result.content) - - if "error" in result_code: - raise Exception(result_code["error"]) - return True - - def fetch_instruments( - self, - expired=False, - instrument_type: InstrumentType = InstrumentType.PERP, - currency: UnderlyingCurrency = UnderlyingCurrency.BTC, - ): - """ - Return the tickers. - First fetch all instruments - Then get the ticket for all instruments. - """ - url = self.endpoints.public.get_instruments - payload = { - "expired": expired, - "instrument_type": instrument_type.value, - "currency": currency.name, - } - return self._send_request(url, json=payload, headers=PUBLIC_HEADERS) - - def _get_session_keys(self, wallet: Address) -> list[SessionKey]: - url = self.endpoints.private.session_keys - payload = {"wallet": wallet} - session_keys = self._send_request(url, json=payload) - if not (public_session_keys := session_keys.get("public_session_keys")): - msg = f"No session keys registered for this wallet: {wallet}" - raise ValueError(msg) - return list(map(lambda kwargs: SessionKey(**kwargs), public_session_keys)) - - def fetch_subaccounts(self): - """ - Returns the subaccounts for a given wallet - """ - url = self.endpoints.private.get_subaccounts - payload = {"wallet": self.wallet} - return self._send_request(url, json=payload) - - def fetch_subaccount(self, subaccount_id: int): - """ - Returns information for a given subaccount - """ - url = self.endpoints.private.get_subaccount - payload = {"subaccount_id": subaccount_id} - return self._send_request(url, json=payload) - - def _internal_map_instrument(self, instrument_type, currency): - """ - Map the instrument. - """ - instruments = self.fetch_instruments(instrument_type=instrument_type, currency=currency) - return {i["instrument_name"]: i for i in instruments} - - def create_order( - self, - amount: int, - instrument_name: str, - price: float = None, - reduce_only=False, - instrument_type: InstrumentType = InstrumentType.PERP, - side: OrderSide = OrderSide.BUY, - order_type: OrderType = OrderType.LIMIT, - time_in_force: TimeInForce = TimeInForce.GTC, - instruments=None, # temporary hack to allow async fetching of instruments - ): - """ - Create the order. - """ - if side.name.upper() not in OrderSide.__members__: - raise Exception(f"Invalid side {side}") - - if not instruments: - _currency = UnderlyingCurrency[instrument_name.split("-")[0]] - if instrument_type in [ - InstrumentType.PERP, - InstrumentType.ERC20, - InstrumentType.OPTION, - ]: - instruments = self._internal_map_instrument(instrument_type, _currency) - else: - raise Exception(f"Invalid instrument type {instrument_type}") - - instrument = instruments[instrument_name] - amount_step = instrument["amount_step"] - rounded_amount = Decimal(str(amount)).quantize(Decimal(str(amount_step))) - - if price is not None: - price_step = instrument["tick_size"] - rounded_price = Decimal(str(price)).quantize(Decimal(str(price_step))) - - module_data = { - "asset_address": instrument["base_asset_address"], - "sub_id": int(instrument["base_asset_sub_id"]), - "limit_price": Decimal(str(rounded_price)) if price is not None else Decimal(0), - "amount": Decimal(str(rounded_amount)), - "max_fee": Decimal(1000), - "recipient_id": int(self.subaccount_id), - "is_bid": side == OrderSide.BUY, - } - - signed_action = self._generate_signed_action( - module_address=self.config.contracts.TRADE_MODULE, - module_data=module_data, - ) - - order = { - "instrument_name": instrument_name, - "direction": side.name.lower(), - "order_type": order_type.name.lower(), - "mmp": False, - "time_in_force": time_in_force.value, - "referral_code": DEFAULT_REFERER, - **signed_action.to_json(), - } - - url = self.endpoints.private.order - return self._send_request(url, json=order)["order"] - - def _generate_signed_action( - self, - module_address: str, - module_data: dict, - module_data_class=TradeModuleData, - subaccount_id=None, - ): - """ - Generate the signed action - """ - action = SignedAction( - subaccount_id=self.subaccount_id if subaccount_id is None else subaccount_id, - owner=self.wallet, - signer=self.signer.address, - signature_expiry_sec=MAX_INT_32, - nonce=get_action_nonce(), - module_address=module_address, - module_data=module_data_class(**module_data), - DOMAIN_SEPARATOR=self.config.DOMAIN_SEPARATOR, - ACTION_TYPEHASH=self.config.ACTION_TYPEHASH, - ) - action.sign(self.signer._private_key) - return action - - def submit_order(self, order): - url = self.endpoints.private.order - return self._send_request(url, json=order)["order"] - - def fetch_ticker(self, instrument_name): - """ - Fetch the ticker for a given instrument name. - """ - url = self.endpoints.public.get_ticker - payload = {"instrument_name": instrument_name} - response = requests.post(url, json=payload, headers=PUBLIC_HEADERS) - results = json.loads(response.content)["result"] - return results - - def fetch_tickers( - self, - instrument_type: InstrumentType = InstrumentType.OPTION, - currency: UnderlyingCurrency = UnderlyingCurrency.BTC, - ): - instruments = self.fetch_instruments(instrument_type=instrument_type, currency=currency) - return {inst["instrument_name"]: self.fetch_ticker(inst["instrument_name"]) for inst in instruments} - - def get_order(self, order_id: str) -> dict: - url = self.endpoints.private.get_order - headers = self._create_signature_headers() - payload = { - "order_id": order_id, - "subaccount_id": self.subaccount_id, - } - response = requests.post(url, json=payload, headers=headers) - return response.json()["result"] - - def fetch_orders( - self, - instrument_name: str = None, - label: str = None, - page: int = 1, - page_size: int = 100, - status: OrderStatus = None, - ): - """ - Fetch the orders for a given instrument name. - """ - url = self.endpoints.private.get_orders - payload = { - "instrument_name": instrument_name, - "subaccount_id": self.subaccount_id, - } - for key, value in { - "label": label, - "page": page, - "page_size": page_size, - "status": status, - }.items(): - if value: - payload[key] = value - headers = self._create_signature_headers() - response = requests.post(url, json=payload, headers=headers) - results = response.json()["result"]["orders"] - return results - - def cancel(self, order_id, instrument_name): - """ - Cancel an order - """ - url = self.endpoints.private.cancel - payload = { - "order_id": order_id, - "subaccount_id": self.subaccount_id, - "instrument_name": instrument_name, - } - return self._send_request(url, json=payload) - - def cancel_all(self): - """ - Cancel all orders - """ - url = self.endpoints.private.cancel_all - payload = {"subaccount_id": self.subaccount_id} - return self._send_request(url, json=payload) - - def _check_output_for_rate_limit(self, message): - if (error := message.get("error")) and "Rate limit exceeded" in error["message"]: - sleep((int(error["data"].split(" ")[-2]) / 1000)) - self.logger.info("Rate limit exceeded, sleeping and retrying request") - return True - return False - - def get_positions(self): - """ - Get positions - """ - url = self.endpoints.private.get_positions - payload = {"subaccount_id": self.subaccount_id} - headers = sign_rest_auth_header( - web3_client=self.web3_client, - smart_contract_wallet=self.wallet, - session_key_or_wallet_private_key=self.signer._private_key, - ) - response = requests.post(url, json=payload, headers=headers) - results = response.json()["result"]["positions"] - return results - - def get_collaterals(self): - """ - Get collaterals - """ - url = self.endpoints.private.get_collaterals - payload = {"subaccount_id": self.subaccount_id} - result = self._send_request(url, json=payload) - return result["collaterals"] - - def create_subaccount( - self, - amount: int = 0, - subaccount_type: SubaccountType = SubaccountType.STANDARD, - collateral_asset: CollateralAsset = CollateralAsset.USDC, - underlying_currency: UnderlyingCurrency = UnderlyingCurrency.ETH, - ): - """ - Create a subaccount. - """ - url = self.endpoints.private.create_subaccount - if subaccount_type is SubaccountType.STANDARD: - contract_key = f"{subaccount_type.name}_RISK_MANAGEr" - elif subaccount_type is SubaccountType.PORTFOLIO: - if not collateral_asset: - raise Exception("Underlying currency must be provided for portfolio subaccounts") - contract_key = f"{underlying_currency.name}_{subaccount_type.name}_RISK_MANAGER" - - signed_action = self._generate_signed_action( - module_address=self.config.contracts[contract_key], - module_data={ - "amount": amount, - "asset_name": collateral_asset.name, - "margin_type": "SM" if subaccount_type is SubaccountType.STANDARD else "PM", - "create_account_details": CreateSubAccountDetails( - amount=amount, - base_asset_address=self.config.contracts.CASH_ASSET, - sub_asset_address=self.config.contracts[contract_key], - ), - }, - module_data_class=CreateSubAccountData, - subaccount_id=0, - ) - - payload = { - "amount": str(amount), - "asset_name": collateral_asset.name, - "margin_type": "SM" if subaccount_type is SubaccountType.STANDARD else "PM", - "wallet": self.wallet, - **signed_action.to_json(), - } - if subaccount_type is SubaccountType.PORTFOLIO: - payload["currency"] = underlying_currency.name - del payload["subaccount_id"] - response = self._send_request(url, json=payload) - return response - - def get_nonce_and_signature_expiry(self): - """ - Returns the nonce and signature expiry - """ - ts = utc_now_ms() - nonce = int(f"{int(ts)}{random.randint(100, 999)}") - expiration = int(ts) + 6000 - return ts, nonce, expiration - - def transfer_collateral(self, amount: int, to: str, asset: CollateralAsset): - """ - Transfer collateral - """ - url = self.endpoints.private.transfer_erc20 - transfer_details = TransferERC20Details( - base_address=self.config.contracts.CASH_ASSET, - sub_id=0, - amount=Decimal(amount), - ) - sender_action = SignedAction( - subaccount_id=self.subaccount_id, - owner=self.wallet, - signer=self.signer.address, - signature_expiry_sec=MAX_INT_32, - nonce=get_action_nonce(), - module_address=self.config.contracts.TRANSFER_MODULE, - module_data=SenderTransferERC20ModuleData( - to_subaccount_id=to, - transfers=[transfer_details], - ), - DOMAIN_SEPARATOR=self.config.DOMAIN_SEPARATOR, - ACTION_TYPEHASH=self.config.ACTION_TYPEHASH, - ) - sender_action.sign(self.signer.key) - - recipient_action = SignedAction( - subaccount_id=to, - owner=self.wallet, - signer=self.signer.address, - signature_expiry_sec=MAX_INT_32, - nonce=get_action_nonce(), - module_address=self.config.contracts.TRANSFER_MODULE, - module_data=RecipientTransferERC20ModuleData(), - DOMAIN_SEPARATOR=self.config.DOMAIN_SEPARATOR, - ACTION_TYPEHASH=self.config.ACTION_TYPEHASH, - ) - recipient_action.sign(self.signer.key) - payload = { - "subaccount_id": self.subaccount_id, - "recipient_subaccount_id": to, - "sender_details": { - "nonce": sender_action.nonce, - "signature": sender_action.signature, - "signature_expiry_sec": sender_action.signature_expiry_sec, - "signer": sender_action.signer, - }, - "recipient_details": { - "nonce": recipient_action.nonce, - "signature": recipient_action.signature, - "signature_expiry_sec": recipient_action.signature_expiry_sec, - "signer": recipient_action.signer, - }, - "transfer": { - "address": self.config.contracts.CASH_ASSET, - "amount": str(transfer_details.amount), - "sub_id": str(transfer_details.sub_id), - }, - } - return self._send_request(url, json=payload) - - def get_mmp_config(self, subaccount_id: int, currency: UnderlyingCurrency = None): - """Get the mmp config.""" - url = self.endpoints.private.get_mmp_config - payload = {"subaccount_id": self.subaccount_id} - if currency: - payload["currency"] = currency.name - return self._send_request(url, json=payload) - - def set_mmp_config( - self, - subaccount_id: int, - currency: UnderlyingCurrency, - mmp_frozen_time: int, - mmp_interval: int, - mmp_amount_limit: str, - mmp_delta_limit: str, - ): - """Set the mmp config.""" - url = self.endpoints.private.set_mmp_config - payload = { - "subaccount_id": subaccount_id, - "currency": currency.name, - "mmp_frozen_time": mmp_frozen_time, - "mmp_interval": mmp_interval, - "mmp_amount_limit": mmp_amount_limit, - "mmp_delta_limit": mmp_delta_limit, - } - return self._send_request(url, json=payload) - - def send_rfq(self, rfq): - """Send an RFQ.""" - url = self.endpoints.private.send_rfq - payload = { - **rfq, - "subaccount_id": self.subaccount_id, - } - return self._send_request(url, payload) - - def poll_rfqs(self, rfq_status: RfqStatus | None = None): - """ - Poll RFQs. - type RfqResponse = { - subaccount_id: number, - creation_timestamp: number, - last_update_timestamp: number, - status: string, - cancel_reason: string, - rfq_id: string, - valid_until: number, - legs: Array - } - """ - url = self.endpoints.private.poll_rfqs - params = { - "subaccount_id": self.subaccount_id, - } - if rfq_status: - params["status"] = rfq_status.value - return self._send_request( - url, - json=params, - ) - - def send_quote(self, quote): - """Send a quote.""" - url = self.endpoints.private.send_quote - return self._send_request(url, quote) - - def create_quote( - self, - rfq_id, - legs, - direction, - ): - """Create a quote object.""" - _, nonce, expiration = self.get_nonce_and_signature_expiry() - - rfq_legs: list[RFQQuoteDetails] = [] - for leg in legs: - ticker = self.fetch_ticker(instrument_name=leg["instrument_name"]) - rfq_quote_details = RFQQuoteDetails( - instrument_name=ticker["instrument_name"], - direction=leg["direction"], - asset_address=ticker["base_asset_address"], - sub_id=int(ticker["base_asset_sub_id"]), - price=Decimal(leg["price"]), - amount=Decimal(leg["amount"]), - ) - rfq_legs.append(rfq_quote_details) - - action = SignedAction( - subaccount_id=self.subaccount_id, - owner=self.wallet, - signer=self.signer.address, - signature_expiry_sec=MAX_INT_32, - nonce=nonce, - module_address=self.config.contracts.RFQ_MODULE, - module_data=RFQQuoteModuleData( - global_direction=direction, - max_fee=Decimal(100), - legs=rfq_legs, - ), - DOMAIN_SEPARATOR=self.config.DOMAIN_SEPARATOR, - ACTION_TYPEHASH=self.config.ACTION_TYPEHASH, - ) - - action.sign(self.signer.key) - - payload = { - **action.to_json(), - "label": "", - "mmp": False, - "rfq_id": rfq_id, - } - - return self.send_quote(quote=payload) - - def execute_quote( - self, - quote: RFQQuoteDetails, - rfq_id: str = None, - quote_id: str = None, - ): - """Execute a quote.""" - _, nonce, expiration = self.get_nonce_and_signature_expiry() - - action = SignedAction( - subaccount_id=self.subaccount_id, - owner=self.wallet, - signer=self.signer.address, - signature_expiry_sec=MAX_INT_32, - nonce=nonce, - module_address=self.config.contracts.RFQ_MODULE, - module_data=quote, - DOMAIN_SEPARATOR=self.config.DOMAIN_SEPARATOR, - ACTION_TYPEHASH=self.config.ACTION_TYPEHASH, - ) - action.sign(self.signer.key) - payload = { - **action.to_json(), - "label": "", - "rfq_id": rfq_id, - "quote_id": quote_id, - } - url = self.endpoints.private.execute_quote - return self._send_request(url, json=payload) - - def cancel_rfq(self, rfq_id: str): - """Cancel an RFQ.""" - url = self.endpoints.private.cancel_rfq - payload = { - "subaccount_id": self.subaccount_id, - "rfq_id": rfq_id, - } - return self._send_request(url, json=payload) - - def cancel_batch_rfqs(self, rfq_id: str = None, label: str = None, nonce: int = None): - """Cancel RFQs in batch.""" - url = self.endpoints.private.cancel_batch_rfqs - payload = { - "subaccount_id": self.subaccount_id, - } - if rfq_id: - payload["rfq_id"] = rfq_id - if label: - payload["label"] = label - if nonce: - payload["nonce"] = nonce - return self._send_request(url, json=payload) - - def poll_quotes(self, rfq_id: str = None, quote_id: str = None, status: RfqStatus = None): - url = self.endpoints.private.poll_quotes - payload = { - "subaccount_id": self.subaccount_id, - } - if rfq_id: - payload["rfq_id"] = rfq_id - if quote_id: - payload["quote_id"] = quote_id - if status: - payload["status"] = status.value - - return self._send_request(url, json=payload) - - def _send_request(self, url, json=None, params=None, headers=None): - headers = headers if headers else self._create_signature_headers() - response = requests.post(url, json=json, headers=headers, params=params) - response.raise_for_status() - json_data = response.json() - if error := json_data.get("error"): - raise DeriveJSONRPCException(**error) - else: - return json_data["result"] - - def fetch_all_currencies(self): - """ - Fetch the currency list - """ - url = self.endpoints.public.get_all_currencies - return self._send_request(url, json={}) - - def fetch_currency(self, asset_name): - """ - Fetch the currency list - """ - url = self.endpoints.public.get_currency - payload = {"currency": asset_name} - return self._send_request(url, json=payload) - - def get_transaction(self, transaction_id: str) -> DeriveTxResult: - """Get a transaction by its transaction id.""" - url = self.endpoints.public.get_transaction - payload = {"transaction_id": transaction_id} - return DeriveTxResult(**self._send_request(url, json=payload), transaction_id=transaction_id) - - def transfer_from_funding_to_subaccount(self, amount: int, asset_name: str, subaccount_id: int) -> DeriveTxResult: - """ - Transfer from funding to subaccount - """ - manager_address, underlying_address, decimals = self.get_manager_for_subaccount(subaccount_id, asset_name) - if not manager_address or not underlying_address: - raise Exception(f"Unable to find manager address or underlying address for {asset_name}") - - currency = UnderlyingCurrency[asset_name.upper()] - deposit_module_data = DepositModuleData( - amount=str(amount), - asset=underlying_address, - manager=manager_address, - decimals=decimals, - asset_name=currency.name, - ) - - sender_action = SignedAction( - subaccount_id=self.subaccount_id, - owner=self.wallet, - signer=self.signer.address, - signature_expiry_sec=MAX_INT_32, - nonce=get_action_nonce(), - module_address=self.config.contracts.DEPOSIT_MODULE, - module_data=deposit_module_data, - DOMAIN_SEPARATOR=self.config.DOMAIN_SEPARATOR, - ACTION_TYPEHASH=self.config.ACTION_TYPEHASH, - ) - sender_action.sign(self.signer.key) - payload = { - "amount": str(amount), - "asset_name": currency.name, - "is_atomic_signing": False, - "nonce": sender_action.nonce, - "signature": sender_action.signature, - "signature_expiry_sec": sender_action.signature_expiry_sec, - "signer": sender_action.signer, - "subaccount_id": subaccount_id, - } - url = self.endpoints.private.deposit - - deposit_result = DepositResult(**self._send_request(url, json=payload)) - return wait_until( - self.get_transaction, - condition=_is_final_tx, - transaction_id=deposit_result.transaction_id, - ) - - def get_manager_for_subaccount(self, subaccount_id: int, asset_name): - """ - Look up the manager for a subaccount - - Check if target account is PM or SM - If SM, use the standard manager address - If PM, use the appropriate manager address based on the currency of the subaccount - """ - deposit_currency = UnderlyingCurrency[asset_name.upper()] - currency = self.fetch_currency(asset_name.upper()) - underlying_address = currency["protocol_asset_addresses"]["spot"] - managers = list(map(lambda kwargs: ManagerAddress(**kwargs), currency["managers"])) - manager_by_type = {} - for manager in managers: - manager_by_type.setdefault((manager.margin_type, manager.currency), []).append(manager) - - to_account = self.fetch_subaccount(subaccount_id) - account_currency = None - if to_account.get("currency") != "all": - account_currency = MainnetCurrency[to_account.get("currency")] - - margin_type = MarginType[to_account.get("margin_type")] - - def get_unique_manager(margin_type, currency): - matches = manager_by_type.get((margin_type, currency), []) - if len(matches) != 1: - raise ValueError(f"Expected exactly one ManagerAddress for {(margin_type, currency)}, found {matches}") - return matches[0] - - manager = get_unique_manager(margin_type, account_currency) - if not manager.address or not underlying_address: - raise Exception(f"Unable to find manager address or underlying address for {asset_name}") - return manager.address, underlying_address, TOKEN_DECIMALS[deposit_currency] - - def transfer_from_subaccount_to_funding(self, amount: int, asset_name: str, subaccount_id: int) -> DeriveTxResult: - """ - Transfer from subaccount to funding - """ - manager_address, underlying_address, decimals = self.get_manager_for_subaccount(subaccount_id, asset_name) - if not manager_address or not underlying_address: - raise Exception(f"Unable to find manager address or underlying address for {asset_name}") - - currency = UnderlyingCurrency[asset_name.upper()] - module_data = WithdrawModuleData( - amount=str(amount), - asset=underlying_address, - decimals=decimals, - asset_name=currency.name, - ) - sender_action = SignedAction( - subaccount_id=subaccount_id, - owner=self.wallet, - signer=self.signer.address, - signature_expiry_sec=MAX_INT_32, - nonce=get_action_nonce(), - module_address=self.config.contracts.WITHDRAWAL_MODULE, - module_data=module_data, - DOMAIN_SEPARATOR=self.config.DOMAIN_SEPARATOR, - ACTION_TYPEHASH=self.config.ACTION_TYPEHASH, - ) - sender_action.sign(self.signer.key) - payload = { - "is_atomic_signing": False, - "amount": str(amount), - "asset_name": currency.name, - "nonce": sender_action.nonce, - "signature": sender_action.signature, - "signature_expiry_sec": sender_action.signature_expiry_sec, - "signer": sender_action.signer, - "subaccount_id": subaccount_id, - } - url = self.endpoints.private.withdraw - - withdraw_result = WithdrawResult(**self._send_request(url, json=payload)) - return wait_until( - self.get_transaction, - condition=_is_final_tx, - transaction_id=withdraw_result.transaction_id, - ) - - def transfer_position( - self, - instrument_name: str, - amount: float, - to_subaccount_id: int, - ) -> PositionTransfer: - """ - Transfer a position from the current subaccount to another subaccount. - - Parameters: - instrument_name (str): Instrument to transfer (e.g. 'ETH-PERP'). - amount (float): Amount to transfer. Positive for a long, negative for a short. - to_subaccount_id (int): Destination subaccount id (must be different and present in self.subaccount_ids). - - Returns: - PositionTransfer: Result containing maker/taker order and trade details. - """ - if to_subaccount_id == self.subaccount_id: - raise ValueError("Target subaccount is self") - if to_subaccount_id not in self.subaccount_ids: - raise ValueError(f"Subaccount id {to_subaccount_id} not in {self.subaccount_ids}") - - url = self.endpoints.private.transfer_position - positions = [p for p in self.get_positions() if p["instrument_name"] == instrument_name] - if not len(positions) == 1: - raise ValueError(f"Expected to find a position for {instrument_name}, found: {positions}") - - position = positions[0] - position_amount = float(position["amount"]) - if abs(position_amount) < abs(amount): - raise ValueError(f"Position {position_amount} not sufficient for transfer {amount}") - - ticker = self.fetch_ticker(instrument_name=instrument_name) - mark_price = Decimal(ticker["mark_price"]).quantize(Decimal(ticker["tick_size"])) - base_asset_address = ticker["base_asset_address"] - base_asset_sub_id = int(ticker["base_asset_sub_id"]) - - transfer_amount = Decimal(str(abs(amount))) - original_position_amount = Decimal(str(position_amount)) - - maker_action = SignedAction( - subaccount_id=self.subaccount_id, - owner=self.wallet, - signer=self.signer.address, - signature_expiry_sec=MAX_INT_32, - nonce=get_action_nonce(), - module_address=self.config.contracts.TRADE_MODULE, - module_data=MakerTransferPositionModuleData( - asset_address=base_asset_address, - sub_id=base_asset_sub_id, - limit_price=mark_price, - amount=transfer_amount, - recipient_id=self.subaccount_id, - position_amount=original_position_amount, - ), - DOMAIN_SEPARATOR=self.config.DOMAIN_SEPARATOR, - ACTION_TYPEHASH=self.config.ACTION_TYPEHASH, - ) - - # Small delay to ensure different nonces - time.sleep(0.001) - - taker_action = SignedAction( - subaccount_id=to_subaccount_id, - owner=self.wallet, - signer=self.signer.address, - signature_expiry_sec=MAX_INT_32, - nonce=get_action_nonce(), - module_address=self.config.contracts.TRADE_MODULE, - module_data=TakerTransferPositionModuleData( - asset_address=base_asset_address, - sub_id=base_asset_sub_id, - limit_price=mark_price, - amount=transfer_amount, - recipient_id=to_subaccount_id, - position_amount=original_position_amount, - ), - DOMAIN_SEPARATOR=self.config.DOMAIN_SEPARATOR, - ACTION_TYPEHASH=self.config.ACTION_TYPEHASH, - ) - - maker_action.sign(self.signer.key) - taker_action.sign(self.signer.key) - - maker_params = { - "direction": maker_action.module_data.get_direction(), - "instrument_name": instrument_name, - **maker_action.to_json(), - } - taker_params = { - "direction": taker_action.module_data.get_direction(), - "instrument_name": instrument_name, - **taker_action.to_json(), - } - - payload = { - "wallet": self.wallet, - "maker_params": maker_params, - "taker_params": taker_params, - } - - response_data = self._send_request(url, json=payload) - position_transfer = PositionTransfer(**response_data) - - return position_transfer - - def get_position_amount(self, instrument_name: str, subaccount_id: int) -> float: - """ - Get the current position amount for a specific instrument in a subaccount. - - This is a helper method for getting position amounts to use with transfer_position(). - - Parameters: - instrument_name (str): The name of the instrument. - subaccount_id (int): The subaccount ID to check. - - Returns: - float: The current position amount. - - Raises: - ValueError: If no position found for the instrument in the subaccount. - """ - positions = self.get_positions() - # get_positions() returns a list directly - position_list = positions if isinstance(positions, list) else positions.get("positions", []) - for pos in position_list: - if pos["instrument_name"] == instrument_name: - return float(pos["amount"]) - - raise ValueError(f"No position found for {instrument_name} in subaccount {subaccount_id}") - - def transfer_positions( - self, - positions: list[PositionSpec], # amount, instrument_name - to_subaccount_id: int, - direction: OrderSide, # TransferDirection - ) -> PositionsTransfer: - """ - Transfer multiple positions between subaccounts using RFQ system. - Parameters: - positions (list[TransferPosition]): list of TransferPosition objects containing: - - instrument_name (str): Name of the instrument - - amount (float): Amount to transfer (must be positive) - - limit_price (float): Limit price for the transfer (must be positive) - from_subaccount_id (int): The subaccount ID to transfer from. - to_subaccount_id (int): The subaccount ID to transfer to. - global_direction (str): Global direction for the transfer ("buy" or "sell"). - Returns: - DeriveTxResult: The result of the transfer transaction. - Raises: - ValueError: If positions list is empty, invalid global_direction, or if any instrument not found. - """ - - if to_subaccount_id == self.subaccount_id: - raise ValueError("Target subaccount is self") - if to_subaccount_id not in self.subaccount_ids: - raise ValueError(f"Subaccount id {to_subaccount_id} not in {self.subaccount_ids}") - - url = self.endpoints.private.transfer_positions - current_positions = {p["instrument_name"]: p for p in self.get_positions()} - - transfer_details = [] - for position in positions: - amount = Decimal(str(position.amount)) - instrument_name = position.instrument_name - - if (current_position := current_positions.get(instrument_name)) is None: - available = ", ".join(sorted(current_positions.keys())) or "none" - msg = f"No position for {instrument_name} (available: {available})" - raise ValueError(msg) - - current_amount = Decimal(current_position["amount"]).quantize(Decimal(current_position["amount_step"])) - if abs(current_amount) < abs(amount): - msg = f"Insufficient position for {instrument_name}: have {current_amount}, need {amount}" - raise ValueError(msg) - - ticker = self.fetch_ticker(instrument_name=instrument_name) - mark_price = Decimal(ticker["mark_price"]).quantize(Decimal(ticker["tick_size"])) - base_asset_address = ticker["base_asset_address"] - base_asset_sub_id = int(ticker["base_asset_sub_id"]) - - transfer_amount = Decimal(str(abs(amount))) - - leg_direction = "sell" if amount > 0 else "buy" - transfer_details.append( - TransferPositionsDetails( - instrument_name=instrument_name, - direction=leg_direction, - asset_address=base_asset_address, - sub_id=base_asset_sub_id, - price=mark_price, - amount=transfer_amount, - ) - ) - - # Derive RPC -32602: Invalid params [data=['Legs must be sorted by instrument name']] - transfer_details.sort(key=lambda x: x.instrument_name) - - # Create maker action (sender) - USING RFQ_MODULE, not TRADE_MODULE - maker_action = SignedAction( - subaccount_id=self.subaccount_id, - owner=self.wallet, - signer=self.signer.address, - signature_expiry_sec=MAX_INT_32, - nonce=get_action_nonce(), # maker_nonce - module_address=self.config.contracts.RFQ_MODULE, - module_data=MakerTransferPositionsModuleData( - global_direction=direction.value, - positions=transfer_details, - ), - DOMAIN_SEPARATOR=self.config.DOMAIN_SEPARATOR, - ACTION_TYPEHASH=self.config.ACTION_TYPEHASH, - ) - - # Small delay to ensure different nonces - time.sleep(0.001) - - # Create taker action (recipient) - USING RFQ_MODULE, not TRADE_MODULE - opposite_direction = "sell" if direction.value == "buy" else "buy" - taker_action = SignedAction( - subaccount_id=to_subaccount_id, - owner=self.wallet, - signer=self.signer.address, - signature_expiry_sec=MAX_INT_32, - nonce=get_action_nonce(), - module_address=self.config.contracts.RFQ_MODULE, - module_data=TakerTransferPositionsModuleData( - global_direction=opposite_direction, - positions=transfer_details, - ), - DOMAIN_SEPARATOR=self.config.DOMAIN_SEPARATOR, - ACTION_TYPEHASH=self.config.ACTION_TYPEHASH, - ) - - maker_action.sign(self.signer.key) - taker_action.sign(self.signer.key) - - payload = { - "wallet": self.wallet, - "maker_params": maker_action.to_json(), - "taker_params": taker_action.to_json(), - } - - response_data = self._send_request(url, json=payload) - positions_transfer = PositionsTransfer(**response_data) - - return positions_transfer diff --git a/derive_client/clients/http_client.py b/derive_client/clients/http_client.py deleted file mode 100644 index 387e6be1..00000000 --- a/derive_client/clients/http_client.py +++ /dev/null @@ -1,107 +0,0 @@ -""" -Base class for HTTP client. -""" - -import functools -from logging import Logger, LoggerAdapter - -from derive_client.data_types import Address, BridgeTxResult, ChainID, Currency, Environment, PreparedBridgeTx -from derive_client.utils.asyncio_sync import run_coroutine_sync - -from .async_client import AsyncClient -from .base_client import BaseClient - - -class HttpClient(BaseClient): - """HTTP client class.""" - - def __init__( - self, - wallet: Address, - private_key: str, - env: Environment, - logger: Logger | LoggerAdapter | None = None, - verbose: bool = False, - subaccount_id: int | None = None, - ): - super().__init__( - wallet=wallet, - private_key=private_key, - env=env, - logger=logger, - verbose=verbose, - subaccount_id=subaccount_id, - ) - - @functools.cached_property - def _async_client(self) -> AsyncClient: - return AsyncClient( - wallet=self.wallet, - private_key=self.private_key, - env=self.env, - logger=self.logger, - verbose=self.verbose, - subaccount_id=self.subaccount_id, - ) - - def prepare_standard_tx( - self, - human_amount: float, - currency: Currency, - to: Address, - source_chain: ChainID, - target_chain: ChainID, - ) -> PreparedBridgeTx: - """Thin sync wrapper around AsyncClient.prepare_standard_tx.""" - - coroutine = self._async_client.prepare_standard_tx( - human_amount=human_amount, - currency=currency, - to=to, - source_chain=source_chain, - target_chain=target_chain, - ) - - return run_coroutine_sync(coroutine) - - def prepare_deposit_to_derive( - self, - human_amount: float, - currency: Currency, - chain_id: ChainID, - ) -> PreparedBridgeTx: - """Thin sync wrapper around AsyncClient.prepare_deposit_to_derive.""" - - coroutine = self._async_client.prepare_deposit_to_derive( - human_amount=human_amount, - currency=currency, - chain_id=chain_id, - ) - return run_coroutine_sync(coroutine) - - def prepare_withdrawal_from_derive( - self, - human_amount: float, - currency: Currency, - chain_id: ChainID, - ) -> PreparedBridgeTx: - """Thin sync wrapper around AsyncClient.prepare_withdrawal_from_derive.""" - - coroutine = self._async_client.prepare_withdrawal_from_derive( - human_amount=human_amount, - currency=currency, - chain_id=chain_id, - ) - return run_coroutine_sync(coroutine) - - def submit_bridge_tx(self, prepared_tx: PreparedBridgeTx) -> BridgeTxResult: - """Thin sync wrapper around AsyncClient.submit_bridge_tx.""" - - coroutine = self._async_client.submit_bridge_tx(prepared_tx=prepared_tx) - return run_coroutine_sync(coroutine) - - def poll_bridge_progress(self, tx_result: BridgeTxResult) -> BridgeTxResult: - """Thin sync wrapper around AsyncClient.poll_bridge_progress.""" - - coroutine = self._async_client.poll_bridge_progress(tx_result=tx_result) - return run_coroutine_sync(coroutine) diff --git a/derive_client/clients/ws_client.py b/derive_client/clients/ws_client.py deleted file mode 100644 index b03efd56..00000000 --- a/derive_client/clients/ws_client.py +++ /dev/null @@ -1,440 +0,0 @@ -""" -Class to handle base websocket client -""" - -import json -import time -import uuid -from dataclasses import dataclass -from decimal import Decimal -from enum import StrEnum - -import msgspec -from derive_action_signing.utils import sign_ws_login, utc_now_ms -from websockets import State -from websockets.sync.client import ClientConnection, connect - -from derive_client.constants import DEFAULT_REFERER -from derive_client.data.generated.models import ( - Direction, - OrderResponseSchema, - PrivateGetOrdersResultSchema, - PrivateOrderParamsSchema, - PublicGetTickerResultSchema, - TradeResponseSchema, -) -from derive_client.data_types import InstrumentType, UnderlyingCurrency -from derive_client.data_types.enums import OrderSide, OrderType, TimeInForce -from derive_client.exceptions import DeriveJSONRPCException - -from .base_client import BaseClient - - -@dataclass -class Orderbook: - channel: str - timestamp: int - instrument_name: str - publish_id: int - bids: list[list[float]] - asks: list[list[float]] - - @classmethod - def from_json(cls, data): - return cls( - channel=data["params"]["channel"], - timestamp=data["params"]["data"]["timestamp"], - instrument_name=data["params"]["data"]["instrument_name"], - publish_id=data["params"]["data"]["publish_id"], - bids=[[float(price), float(size)] for price, size in data["params"]["data"]["bids"]], - asks=[[float(price), float(size)] for price, size in data["params"]["data"]["asks"]], - ) - - -@dataclass -class Position: - """ - {'instrument_type': 'perp', 'instrument_name': 'ETH-PERP', 'amount': '0', 'average_price': '0', 'realized_pnl': '0', - 'unrealized_pnl': '0', 'total_fees': '0', 'average_price_excl_fees': '0', 'realized_pnl_excl_fees': '0', 'unrealized_pnl_excl_fees': '0', - 'net_settlements': '0', 'cumulative_funding': '0', 'pending_funding': '0', 'mark_price': '4153.1395224770267304847948253154754638671875', - 'index_price': '4156.522924638571', 'delta': '1', 'gamma': '0', 'vega': '0', 'theta': '0', 'mark_value': '0', 'maintenance_margin': '0', - 'initial_margin': '0', 'open_orders_margin': '-81.7268423838896751476568169891834259033203125', 'leverage': None, 'liquidation_price': None, - 'creation_timestamp': 0, 'amount_step': '0'} - """ - - instrument_type: str | None = None - instrument_name: str | None = None - amount: float | None = None - average_price: float | None = None - realized_pnl: float | None = None - unrealized_pnl: float | None = None - total_fees: float | None = None - average_price_excl_fees: float | None = None - realized_pnl_excl_fees: float | None = None - unrealized_pnl_excl_fees: float | None = None - net_settlements: float | None = None - cumulative_funding: float | None = None - pending_funding: float | None = None - mark_price: float | None = None - index_price: float | None = None - delta: float | None = None - gamma: float | None = None - vega: float | None = None - theta: float | None = None - mark_value: float | None = None - maintenance_margin: float | None = None - initial_margin: float | None = None - open_orders_margin: float | None = None - leverage: float | None = None - liquidation_price: float | None = None - creation_timestamp: int | None = None - amount_step: float | None = None - - @classmethod - def from_json(cls, data): - return cls( - instrument_type=data.get("instrument_type"), - instrument_name=data.get("instrument_name"), - amount=float(data.get("amount", 0)) if data.get("amount") is not None else None, - average_price=float(data.get("average_price", 0)) if data.get("average_price") is not None else None, - realized_pnl=float(data.get("realized_pnl", 0)) if data.get("realized_pnl") is not None else None, - unrealized_pnl=float(data.get("unrealized_pnl", 0)) if data.get("unrealized_pnl") is not None else None, - total_fees=float(data.get("total_fees", 0)) if data.get("total_fees") is not None else None, - average_price_excl_fees=float(data.get("average_price_excl_fees", 0)) - if data.get("average_price_excl_fees") is not None - else None, - realized_pnl_excl_fees=float(data.get("realized_pnl_excl_fees", 0)) - if data.get("realized_pnl_excl_fees") is not None - else None, - unrealized_pnl_excl_fees=float(data.get("unrealized_pnl_excl_fees", 0)) - if data.get("unrealized_pnl_excl_fees") is not None - else None, - net_settlements=float(data.get("net_settlements", 0)) if data.get("net_settlements") is not None else None, - cumulative_funding=float(data.get("cumulative_funding", 0)) - if data.get("cumulative_funding") is not None - else None, - pending_funding=float(data.get("pending_funding", 0)) if data.get("pending_funding") is not None else None, - mark_price=float(data.get("mark_price", 0)) if data.get("mark_price") is not None else None, - index_price=float(data.get("index_price", 0)) if data.get("index_price") is not None else None, - delta=float(data.get("delta", 0)) if data.get("delta") is not None else None, - gamma=float(data.get("gamma", 0)) if data.get("gamma") is not None else None, - vega=float(data.get("vega", 0)) if data.get("vega") is not None else None, - theta=float(data.get("theta", 0)) if data.get("theta") is not None else None, - mark_value=float(data.get("mark_value", 0)) if data.get("mark_value") is not None else None, - maintenance_margin=float(data.get("maintenance_margin", 0)) - if data.get("maintenance_margin") is not None - else None, - initial_margin=float(data.get("initial_margin", 0)) if data.get("initial_margin") is not None else None, - open_orders_margin=float(data.get("open_orders_margin", 0)) - if data.get("open_orders_margin") is not None - else None, - leverage=float(data.get("leverage")) if data.get("leverage") is not None else None, - liquidation_price=float(data.get("liquidation_price")) - if data.get("liquidation_price") is not None - else None, - creation_timestamp=int(data.get("creation_timestamp", 0)) - if data.get("creation_timestamp") is not None - else None, - amount_step=float(data.get("amount_step", 0)) if data.get("amount_step") is not None else None, - ) - - -@dataclass -class Positions: - positions: list[Position] - subaccount_id: str - - @classmethod - def from_json(cls, data): - return cls( - positions=[Position.from_json(pos) for pos in data], - subaccount_id=data["subaccount_id"], - ) - - -class Depth(StrEnum): - DEPTH_1 = "1" - DEPTH_10 = "10" - DEPTH_20 = "20" - DEPTH_100 = "100" - - -class Group(StrEnum): - GROUP_1 = "1" - GROUP_10 = "10" - GROUP_100 = "100" - - -class Interval(StrEnum): - ONE_HUNDRED_MS = "100" - ONE_SECOND = "1000" - - -class WsClient(BaseClient): - """Websocket client class.""" - - _ws: ClientConnection | None = None - subsriptions: dict = {} - requests_in_flight: dict = {} - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.login_client() - - def connect_ws(self): - return connect( - self.config.ws_address, - ) - - @property - def ws(self): - if self._ws is None: - self._ws = self.connect_ws() - if self._ws.state is not State.OPEN: - self._ws = self.connect_ws() - return self._ws - - def login_client( - self, - retries=3, - ): - login_request = { - "method": "public/login", - "params": sign_ws_login( - web3_client=self.web3_client, - smart_contract_wallet=self.wallet, - session_key_or_wallet_private_key=self.signer._private_key, - ), - "id": str(utc_now_ms()), - } - try: - self.ws.send(json.dumps(login_request)) - # we need to wait for the response - while True: - message = json.loads(self.ws.recv()) - if message["id"] == login_request["id"]: - if "result" not in message: - if self._check_output_for_rate_limit(message): - return self.login_client() - raise DeriveJSONRPCException(**message["error"]) - break - except Exception as error: - if retries: - time.sleep(1) - self.login_client(retries=retries - 1) - raise error - - def create_order( - self, - amount: int, - instrument_name: str, - price: float = None, - reduce_only=False, - instrument_type: InstrumentType = InstrumentType.PERP, - side: OrderSide = OrderSide.BUY, - order_type: OrderType = OrderType.LIMIT, - time_in_force: TimeInForce = TimeInForce.GTC, - instruments=None, # temporary hack to allow async fetching of instruments - ) -> OrderResponseSchema: - """ - Create the order. - """ - if side.name.upper() not in OrderSide.__members__: - raise Exception(f"Invalid side {side}") - - if not instruments: - _currency = UnderlyingCurrency[instrument_name.split("-")[0]] - if instrument_type in [ - InstrumentType.PERP, - InstrumentType.ERC20, - InstrumentType.OPTION, - ]: - instruments = self._internal_map_instrument(instrument_type, _currency) - else: - raise Exception(f"Invalid instrument type {instrument_type}") - - instrument = instruments[instrument_name] - amount_step = instrument["amount_step"] - rounded_amount = Decimal(str(amount)).quantize(Decimal(str(amount_step))) - - if price is not None: - price_step = instrument["tick_size"] - rounded_price = Decimal(str(price)).quantize(Decimal(str(price_step))) - - module_data = { - "asset_address": instrument["base_asset_address"], - "sub_id": int(instrument["base_asset_sub_id"]), - "limit_price": Decimal(str(rounded_price)) if price is not None else Decimal(0), - "amount": Decimal(str(rounded_amount)), - "max_fee": Decimal(1000), - "recipient_id": int(self.subaccount_id), - "is_bid": side == Direction.buy, - } - - signed_action = self._generate_signed_action( - module_address=self.config.contracts.TRADE_MODULE, module_data=module_data - ) - - order = { - "instrument_name": instrument_name, - "direction": side.name.lower(), - "order_type": order_type.name.lower(), - "mmp": False, - "time_in_force": time_in_force.value, - "referral_code": DEFAULT_REFERER, - **signed_action.to_json(), - } - _id = str(uuid.uuid4()) - self.ws.send(json.dumps({"method": "private/order", "params": order, "id": _id})) - self.requests_in_flight[_id] = self._parse_order_message - return PrivateOrderParamsSchema(**order) - - def subscribe_orderbook(self, instrument_name, group: Group = Group.GROUP_1, depth: Depth = Depth.DEPTH_1): - """ - Subscribe to an orderbook feed. - """ - msg = f"orderbook.{instrument_name}.{group}.{depth}" - self.ws.send(json.dumps({"method": "subscribe", "params": {"channels": [msg]}, "id": str(utc_now_ms())})) - self.subsriptions[msg] = self._parse_orderbook_message - - def subscribe_trades(self): - """ - Subscribe to trades feed. - """ - msg = f"{self.subaccount_id}.trades" - self.ws.send(json.dumps({"method": "subscribe", "params": {"channels": [msg]}, "id": str(utc_now_ms())})) - self.subsriptions[msg] = self._parse_trades_message - - def subscribe_orders(self): - """ - Subscribe to orders feed. - """ - msg = f"{self.subaccount_id}.orders" - self.ws.send(json.dumps({"method": "subscribe", "params": {"channels": [msg]}, "id": str(utc_now_ms())})) - self.subsriptions[msg] = self._parse_orders_stream - - def subscribe_ticker(self, instrument_name, interval: Interval = Interval.ONE_HUNDRED_MS): - """ - Subscribe to a ticker feed. - """ - msg = f"ticker.{instrument_name}.{interval}" - self.ws.send(json.dumps({"method": "subscribe", "params": {"channels": [msg]}, "id": str(utc_now_ms())})) - self.subsriptions[msg] = self._parse_ticker_stream - - def _parse_ticker_stream(self, json_message): - """ - Parse a ticker message. - """ - return PublicGetTickerResultSchema(**json_message["params"]["data"]) - - def _parse_orderbook_message(self, json_message): - """ - Parse an orderbook message. - """ - return Orderbook.from_json(json_message) - - def _parse_trades_message(self, json_message): - """ - Parse a trades message. - """ - return TradeResponseSchema.from_json(json_message["params"]["data"]) - - def _parse_order_message(self, json_message): - """ - Parse an orders message. - """ - result = json_message.get("result", None) - if result is None: - raise Exception(f"Invalid order message {json_message}") - if "order" not in result: - return msgspec.convert(json_message["result"], OrderResponseSchema) - return msgspec.convert(json_message["result"]["order"], OrderResponseSchema) - - def _parse_orders_stream(self, json_message): - """ - Parse an orders message. - """ - return msgspec.convert( - {"subaccount_id": self.subaccount_id, "orders": json_message['params']['data']}, - PrivateGetOrdersResultSchema, - ) - - def _parse_orders_message(self, json_message): - """ - Parse an orders message. - """ - return msgspec.convert(json_message['result'], PrivateGetOrdersResultSchema) - - def get_positions(self): - """ - Get positions - """ - id = str(uuid.uuid4()) - payload = {"subaccount_id": self.subaccount_id} - self.ws.send(json.dumps({"method": "private/get_positions", "params": payload, "id": id})) - self.requests_in_flight[id] = self._parse_positions_response - - def get_orders(self): - """ - Get orders - """ - id = str(uuid.uuid4()) - payload = {"subaccount_id": self.subaccount_id} - self.ws.send(json.dumps({"method": "private/get_open_orders", "params": payload, "id": id})) - self.requests_in_flight[id] = self._parse_orders_message - - def _parse_positions_response(self, json_message): - """ - Parse a positions response message. - """ - return Positions( - [Position.from_json(pos) for pos in json_message["result"]["positions"]], - subaccount_id=json_message["result"]["subaccount_id"], - ) - - def parse_message(self, raw_message): - """ - find the parser based on the message type. - """ - json_message = json.loads(raw_message) - if "method" in json_message and json_message["method"] == "subscription": - channel = json_message["params"]["channel"] - if channel in self.subsriptions: - return self.subsriptions[channel](json_message) - raise Exception(f"Unknown channel {channel}") - if "id" in json_message and json_message["id"] in self.requests_in_flight: - parser = self.requests_in_flight.pop(json_message["id"]) - return parser(json_message) - return json_message - - def cancel(self, order_id, instrument_name): - """ - Cancel an order - """ - - id = str(uuid.uuid4()) - payload = { - "order_id": order_id, - "subaccount_id": self.subaccount_id, - "instrument_name": instrument_name, - } - self.ws.send(json.dumps({"method": "private/cancel", "params": payload, "id": id})) - self.requests_in_flight[id] = self._parse_order_cancel_message - - def _parse_order_cancel_message(self, json_message): - """ - Parse an order cancel message. - """ - error = json_message.get("error", None) - if error and error.get("code") in [11006, -32603]: - return - result = json_message.get("result", None) - return OrderResponseSchema(**result) - - def cancel_all(self): - """ - Cancel all orders - """ - id = str(uuid.uuid4()) - payload = {"subaccount_id": self.subaccount_id} - self.ws.send(json.dumps({"method": "private/cancel_all", "params": payload, "id": id})) diff --git a/derive_client/derive.py b/derive_client/derive.py deleted file mode 100644 index fdf693c0..00000000 --- a/derive_client/derive.py +++ /dev/null @@ -1,26 +0,0 @@ -""" -Derive is a Python library for trading on Derive. -""" - -import pandas as pd -from web3 import Web3 - -from derive_client.clients import HttpClient - -# we set to show 4 decimal places -pd.options.display.float_format = '{:,.4f}'.format - - -def to_32byte_hex(val): - return Web3.to_hex(Web3.to_bytes(val).rjust(32, b"\0")) - - -class DeriveClient(HttpClient): - """Client for the derive dex.""" - - def _create_signature_headers(self): - """Generate the signature headers.""" - return HttpClient._create_signature_headers(self) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) From ac66e905d54e9bce5dd0c9e4731fe79cff27752c Mon Sep 17 00:00:00 2001 From: zarathustra Date: Thu, 30 Oct 2025 20:10:19 +0100 Subject: [PATCH 03/22] chore: remove obsolute logger.py --- derive_client/_clients/logger.py | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 derive_client/_clients/logger.py diff --git a/derive_client/_clients/logger.py b/derive_client/_clients/logger.py deleted file mode 100644 index 0c8a7eaa..00000000 --- a/derive_client/_clients/logger.py +++ /dev/null @@ -1,4 +0,0 @@ -import logging - -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) From bbddafdbd5a25e036d118f930643b4dc589f6fd1 Mon Sep 17 00:00:00 2001 From: zarathustra Date: Thu, 30 Oct 2025 20:28:33 +0100 Subject: [PATCH 04/22] fix: pass logger from (Async)HTTPClient to (Async)HTTPSession --- derive_client/_clients/rest/async_http/client.py | 2 +- derive_client/_clients/rest/async_http/session.py | 13 +++++++------ derive_client/_clients/rest/http/client.py | 2 +- derive_client/_clients/rest/http/session.py | 12 ++++++------ 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/derive_client/_clients/rest/async_http/client.py b/derive_client/_clients/rest/async_http/client.py index 8a65dfe3..e6b6900d 100644 --- a/derive_client/_clients/rest/async_http/client.py +++ b/derive_client/_clients/rest/async_http/client.py @@ -56,8 +56,8 @@ def __init__( self._config = config self._subaccount_id = subaccount_id - self._session = AsyncHTTPSession(request_timeout=request_timeout) self._logger = logger if logger is not None else get_logger() + self._session = AsyncHTTPSession(request_timeout=request_timeout, logger=self._logger) self._public_api = AsyncPublicAPI(session=self._session, config=config) self._private_api = AsyncPrivateAPI(session=self._session, config=config, auth=auth) diff --git a/derive_client/_clients/rest/async_http/session.py b/derive_client/_clients/rest/async_http/session.py index 15398746..ce6dfab1 100644 --- a/derive_client/_clients/rest/async_http/session.py +++ b/derive_client/_clients/rest/async_http/session.py @@ -1,10 +1,10 @@ import asyncio import contextvars import weakref +from logging import Logger import aiohttp -from derive_client._clients.logger import logger from derive_client.constants import PUBLIC_HEADERS # Context-local timeout (task-scoped) used to temporarily override session timeout. @@ -14,8 +14,9 @@ class AsyncHTTPSession: - def __init__(self, request_timeout: float): + def __init__(self, request_timeout: float, logger: Logger): self._request_timeout = request_timeout + self._logger = logger self._connector: aiohttp.TCPConnector | None = None self._aiohttp_session: aiohttp.ClientSession | None = None @@ -54,13 +55,13 @@ async def close(self): try: await session.close() except Exception: - logger.exception("Error closing session") + self._logger.exception("Error closing session") if connector and not connector.closed: try: await connector.close() except Exception: - logger.exception("Error closing connector") + self._logger.exception("Error closing connector") async def _send_request( self, @@ -84,13 +85,13 @@ async def _send_request( except Exception as e: raise ValueError(f"Failed to read response from {url}: {e}") from e except aiohttp.ClientError as e: - logger.error("HTTP request failed: %s -> %s", url, e) + self._logger.error("HTTP request failed: %s -> %s", url, e) raise def _finalize(self): if self._aiohttp_session and not self._aiohttp_session.closed: msg = "%s was garbage collected with an open session. Session will be closed by process exit if needed." - logger.debug(msg, self.__class__.__name__) + self._logger.debug(msg, self.__class__.__name__) async def __aenter__(self): await self.open() diff --git a/derive_client/_clients/rest/http/client.py b/derive_client/_clients/rest/http/client.py index 3b33fb1f..f541b453 100644 --- a/derive_client/_clients/rest/http/client.py +++ b/derive_client/_clients/rest/http/client.py @@ -55,8 +55,8 @@ def __init__( self._config = config self._subaccount_id = subaccount_id - self._session = HTTPSession(request_timeout=request_timeout) self._logger = logger if logger is not None else get_logger() + self._session = HTTPSession(request_timeout=request_timeout, logger=self._logger) self._public_api = PublicAPI(session=self._session, config=config) self._private_api = PrivateAPI(session=self._session, config=config, auth=auth) diff --git a/derive_client/_clients/rest/http/session.py b/derive_client/_clients/rest/http/session.py index 6307c51b..4ca921ac 100644 --- a/derive_client/_clients/rest/http/session.py +++ b/derive_client/_clients/rest/http/session.py @@ -1,18 +1,18 @@ from __future__ import annotations import weakref +from logging import Logger import requests from requests.adapters import HTTPAdapter, Retry -from derive_client._clients.logger import logger - class HTTPSession: """HTTP session.""" - def __init__(self, request_timeout: float): + def __init__(self, request_timeout: float, logger: Logger): self._request_timeout = request_timeout + self._logger = logger self._requests_session: requests.Session | None = None self._finalizer = weakref.finalize(self, self._finalize) @@ -68,7 +68,7 @@ def _send_request( response = self._requests_session.post(url, data=data, headers=headers, timeout=timeout) response.raise_for_status() except requests.RequestException as e: - logger.error("HTTP request failed: %s -> %s", url, e) + self._logger.error("HTTP request failed: %s -> %s", url, e) raise return response.content @@ -76,11 +76,11 @@ def _send_request( def _finalize(self): if self._requests_session: msg = "%s was garbage collected without explicit close(); closing session automatically" - logger.debug(msg, self.__class__.__name__) + self._logger.debug(msg, self.__class__.__name__) try: self._requests_session.close() except Exception: - logger.exception("Error closing session in finalizer") + self._logger.exception("Error closing session in finalizer") self._requests_session = None def __enter__(self): From e6275f1992f09fce79bdeeebb242223732964e76 Mon Sep 17 00:00:00 2001 From: zarathustra Date: Thu, 30 Oct 2025 20:42:33 +0100 Subject: [PATCH 05/22] fix: endpoints.py.jinja template --- derive_client/_clients/rest/endpoints.py | 16 ++++++++++++---- derive_client/_clients/rest/http/subaccount.py | 2 +- derive_client/_clients/utils.py | 4 ++-- derive_client/data/templates/endpoints.py.jinja | 16 ++++++++++++---- 4 files changed, 27 insertions(+), 11 deletions(-) diff --git a/derive_client/_clients/rest/endpoints.py b/derive_client/_clients/rest/endpoints.py index 1ac241cd..2a4e4206 100644 --- a/derive_client/_clients/rest/endpoints.py +++ b/derive_client/_clients/rest/endpoints.py @@ -1,10 +1,12 @@ -"""Auto-generated endpoint definitions from OpenAPI spec""" +"""Auto-generated endpoint definitions from OpenAPI spec.""" -from typing import Any +from __future__ import annotations + +from typing import Any, overload class Endpoint: - """Descriptor that provides both REST URLs and WebSocket method names""" + """Descriptor that provides both REST URLs and WebSocket method names.""" def __init__(self, section: str, path: str): self.section = section @@ -15,7 +17,13 @@ def url(self, base_url: str) -> str: """Returns full URL for REST""" return f"{base_url.rstrip('/')}/{self.method}" - def __get__(self, inst: Any, owner: Any): + @overload + def __get__(self, inst: None, owner: type) -> Endpoint: ... + + @overload + def __get__(self, inst: object, owner: type) -> str: ... + + def __get__(self, inst: Any, owner: Any) -> Endpoint | str: if inst is None: return self # Allow class-level access to .method return self.url(inst._base_url) diff --git a/derive_client/_clients/rest/http/subaccount.py b/derive_client/_clients/rest/http/subaccount.py index 234d5091..afe5a9c8 100644 --- a/derive_client/_clients/rest/http/subaccount.py +++ b/derive_client/_clients/rest/http/subaccount.py @@ -173,7 +173,7 @@ def mmp(self) -> MMPOperations: def sign_action( self, *, - module_address: Address, + module_address: Address | str, module_data: ModuleData, signature_expiry_sec: Optional[int] = None, nonce: Optional[int] | None = None, diff --git a/derive_client/_clients/utils.py b/derive_client/_clients/utils.py index b5bda7fb..e97cbdec 100644 --- a/derive_client/_clients/utils.py +++ b/derive_client/_clients/utils.py @@ -71,13 +71,13 @@ def signed_headers(self): def sign_action( self, - module_address: Address, + module_address: Address | str, module_data: ModuleData, subaccount_id: int, signature_expiry_sec: Optional[int] = None, nonce: Optional[int] = None, ) -> SignedAction: - module_address = self.w3.to_checksum_address(module_address) + """Sign action using v2-action-signing library.""" nonce = nonce or time.time_ns() signature_expiry_sec = signature_expiry_sec or get_default_signature_expiry_sec() diff --git a/derive_client/data/templates/endpoints.py.jinja b/derive_client/data/templates/endpoints.py.jinja index 1ffb3cc0..f3e5cfe3 100644 --- a/derive_client/data/templates/endpoints.py.jinja +++ b/derive_client/data/templates/endpoints.py.jinja @@ -1,10 +1,12 @@ -"""Auto-generated endpoint definitions from OpenAPI spec""" +"""Auto-generated endpoint definitions from OpenAPI spec.""" -from typing import Any +from __future__ import annotations + +from typing import Any, overload class Endpoint: - """Descriptor that provides both REST URLs and WebSocket method names""" + """Descriptor that provides both REST URLs and WebSocket method names.""" def __init__(self, section: str, path: str): self.section = section @@ -15,7 +17,13 @@ class Endpoint: """Returns full URL for REST""" return f"{base_url.rstrip('/')}/{self.method}" - def __get__(self, inst: Any, owner: Any): + @overload + def __get__(self, inst: None, owner: type) -> Endpoint: ... + + @overload + def __get__(self, inst: object, owner: type) -> str: ... + + def __get__(self, inst: Any, owner: Any) -> Endpoint | str: if inst is None: return self # Allow class-level access to .method return self.url(inst._base_url) From ae47e332f53c0fe53645967d201fd8b80daf1439 Mon Sep 17 00:00:00 2001 From: zarathustra Date: Fri, 31 Oct 2025 11:48:34 +0100 Subject: [PATCH 06/22] feat: pyright --- poetry.lock | 35 ++++++++++++++++++++++++++++++++++- pyproject.toml | 9 +++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 4fe505ec..d4999a99 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2206,6 +2206,18 @@ files = [ {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, ] +[[package]] +name = "nodeenv" +version = "1.9.1" +description = "Node.js virtual environment builder" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +files = [ + {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, + {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, +] + [[package]] name = "numpy" version = "1.26.4" @@ -2860,6 +2872,27 @@ pyyaml = "*" [package.extras] extra = ["pygments (>=2.19.1)"] +[[package]] +name = "pyright" +version = "1.1.407" +description = "Command line wrapper for pyright" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "pyright-1.1.407-py3-none-any.whl", hash = "sha256:6dd419f54fcc13f03b52285796d65e639786373f433e243f8b94cf93a7444d21"}, + {file = "pyright-1.1.407.tar.gz", hash = "sha256:099674dba5c10489832d4a4b2d302636152a9a42d317986c38474c76fe562262"}, +] + +[package.dependencies] +nodeenv = ">=1.6.0" +typing-extensions = ">=4.1" + +[package.extras] +all = ["nodejs-wheel-binaries", "twine (>=3.4.1)"] +dev = ["twine (>=3.4.1)"] +nodejs = ["nodejs-wheel-binaries"] + [[package]] name = "pytest" version = "7.4.4" @@ -4324,4 +4357,4 @@ propcache = ">=0.2.1" [metadata] lock-version = "2.1" python-versions = ">=3.11,<=3.13" -content-hash = "767c40de9af1bd5a804df5cd8950e7a970522f532c131de4f70ec778224684a0" +content-hash = "fa15635eb056cde0dfdfeb0a4f6dded7f9fda3a6975f2dbe95b98518624bc7ca" diff --git a/pyproject.toml b/pyproject.toml index 0fcacfdf..ba1cc0ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,7 @@ ruff = "^0.13.0" datamodel-code-generator = "^0.34.0" libcst = "^1.8.5" pytest-asyncio = "<1.0.0" +pyright = "^1.1.407" [tool.poetry.group.docs.dependencies] mkdocs = "^1.6.1" @@ -48,6 +49,14 @@ mkdocs-mermaid2-plugin = "^1.2.2" requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" +[tool.pyright] +include = ["derive_client", "tests"] +exclude = ["**/__pycache__", "derive_client/data/generated"] +typeCheckingMode = "basic" +reportMissingImports = true +reportMissingTypeStubs = false +pythonVersion = "3.11" + [tool.ruff] line-length = 120 target-version = "py313" From 55643d28fd45d939ba6c6cb7e0a082d41344326a Mon Sep 17 00:00:00 2001 From: zarathustra Date: Fri, 31 Oct 2025 11:49:44 +0100 Subject: [PATCH 07/22] fix: pyright type-check failures in _clients --- derive_client/_clients/__init__.py | 9 ++++++ .../_clients/rest/async_http/account.py | 28 ++++++++++++++----- .../_clients/rest/async_http/client.py | 8 +++--- .../_clients/rest/async_http/positions.py | 10 +++---- derive_client/_clients/rest/async_http/rfq.py | 8 +++--- .../_clients/rest/async_http/session.py | 11 ++++---- .../_clients/rest/async_http/subaccount.py | 2 +- .../_clients/rest/async_http/transactions.py | 14 ++++++---- derive_client/_clients/rest/http/account.py | 28 ++++++++++++++----- derive_client/_clients/rest/http/client.py | 23 +++++++-------- derive_client/_clients/rest/http/positions.py | 10 +++---- derive_client/_clients/rest/http/rfq.py | 8 +++--- derive_client/_clients/rest/http/session.py | 9 +++--- .../_clients/rest/http/transactions.py | 14 ++++++---- derive_client/_clients/utils.py | 10 ++++--- 15 files changed, 119 insertions(+), 73 deletions(-) create mode 100644 derive_client/_clients/__init__.py diff --git a/derive_client/_clients/__init__.py b/derive_client/_clients/__init__.py new file mode 100644 index 00000000..3fb8d49d --- /dev/null +++ b/derive_client/_clients/__init__.py @@ -0,0 +1,9 @@ +"""Clients module""" + +from .rest.http.client import HTTPClient +from .rest.async_http.client import AsyncHTTPClient + +__all__ = [ + "HTTPClient", + "AsyncHTTPClient", +] diff --git a/derive_client/_clients/rest/async_http/account.py b/derive_client/_clients/rest/async_http/account.py index ba46bada..08432b46 100644 --- a/derive_client/_clients/rest/async_http/account.py +++ b/derive_client/_clients/rest/async_http/account.py @@ -104,10 +104,11 @@ async def from_api( session_keys_response = await private_api.session_keys(session_keys_params) valid_signers = {key.public_session_key: key for key in session_keys_response.result.public_session_keys} - if auth.account.address not in valid_signers: - logger.warning(f"Session key {auth.account.address} is not registered for wallet {auth.wallet}") + signer_address = auth.account.address # type: ignore[attr-defined] + if signer_address not in valid_signers: + logger.warning(f"Session key {signer_address} is not registered for wallet {auth.wallet}") else: - logger.debug(f"Session key validated: {auth.account.address}") + logger.debug(f"Session key validated: {signer_address}") return cls( auth=auth, @@ -118,11 +119,26 @@ async def from_api( _state=state, ) + @property + def state(self) -> PrivateGetAccountResultSchema: + """Current mutable state.""" + if not self._state: + msg = "Account state not loaded. Use Account.from_api() to instantiate or call refresh() to load state." + raise RuntimeError(msg) + return self._state + @property def address(self) -> Address: """LightAccount wallet address.""" return self._auth.wallet + async def refresh(self) -> LightAccount: + """Refresh mutable state from API.""" + params = PrivateGetAccountParamsSchema(wallet=self._auth.wallet) + response = await self._private_api.get_account(params) + self._state = response.result + return self + async def build_register_session_key_tx( self, *, @@ -197,7 +213,7 @@ async def register_scoped_session_key( public_session_key: str, ip_whitelist: Optional[list[str]] = None, label: Optional[str] = None, - scope: Scope = 'read_only', + scope: Scope = Scope.read_only, signed_raw_tx: Optional[str] = None, ) -> PrivateRegisterScopedSessionKeyResultSchema: params = PrivateRegisterScopedSessionKeyParamsSchema( @@ -267,7 +283,6 @@ async def create_subaccount( if margin_type == MarginType.SM and currency is not None: raise ValueError("base_currency must not be provided for standard-margin (SM) subaccounts.") - nonce = nonce if nonce is not None else self._auth.nonce_generator.next() subaccount_id = 0 # must be zero for new account creation module_address = self._config.contracts.DEPOSIT_MODULE @@ -276,7 +291,7 @@ async def create_subaccount( asset = self._config.contracts.CASH_ASSET module_data = DepositModuleData( - amount=str(amount), + amount=amount, asset=asset, manager=manager_address, decimals=decimals, @@ -284,7 +299,6 @@ async def create_subaccount( ) signed_action = self._auth.sign_action( - nonce=nonce, module_address=module_address, module_data=module_data, signature_expiry_sec=signature_expiry_sec, diff --git a/derive_client/_clients/rest/async_http/client.py b/derive_client/_clients/rest/async_http/client.py index e6b6900d..50c3e258 100644 --- a/derive_client/_clients/rest/async_http/client.py +++ b/derive_client/_clients/rest/async_http/client.py @@ -5,7 +5,7 @@ from logging import Logger from typing import AsyncGenerator -from pydantic import validate_call +from pydantic import ConfigDict, validate_call from web3 import AsyncWeb3 from derive_client._bridge.async_client import AsyncBridgeClient @@ -29,7 +29,7 @@ class AsyncHTTPClient: """Asynchronous HTTP client""" - @validate_call(config=dict(arbitrary_types_allowed=True)) + @validate_call(config=ConfigDict(arbitrary_types_allowed=True)) def __init__( self, *, @@ -41,7 +41,7 @@ def __init__( request_timeout: float = 10.0, ): config = CONFIGS[env] - w3 = AsyncWeb3(AsyncWeb3.HTTPProvider(config.rpc_endpoint)) + w3 = AsyncWeb3(AsyncWeb3.AsyncHTTPProvider(config.rpc_endpoint)) account = w3.eth.account.from_key(session_key) auth = AuthContext( @@ -103,7 +103,7 @@ async def connect(self, initialize_bridge: bool = True) -> None: elif initialize_bridge: self._logger.debug("Bridge module unavailable in non-prod environment.") - subaccount_ids = self._light_account._state.subaccount_ids + subaccount_ids = self._light_account.state.subaccount_ids if self._subaccount_id not in subaccount_ids: self._logger.warning( f"Subaccount {self._subaccount_id} does not exist for wallet {self._light_account.address}. " diff --git a/derive_client/_clients/rest/async_http/positions.py b/derive_client/_clients/rest/async_http/positions.py index fd7494ff..428fd1df 100644 --- a/derive_client/_clients/rest/async_http/positions.py +++ b/derive_client/_clients/rest/async_http/positions.py @@ -105,7 +105,7 @@ async def transfer( maker_params = TradeModuleParamsSchema( amount=abs(amount), - direction=maker_module_data.get_direction(), + direction=Direction[maker_module_data.get_direction()], instrument_name=instrument_name, limit_price=limit_price, max_fee=max_fee, @@ -117,7 +117,7 @@ async def transfer( ) taker_params = TradeModuleParamsSchema( amount=abs(amount), - direction=taker_module_data.get_direction(), + direction=Direction[taker_module_data.get_direction()], instrument_name=instrument_name, limit_price=limit_price, max_fee=max_fee, @@ -174,7 +174,7 @@ async def transfer_batch( details = TransferPositionsDetails( instrument_name=instrument_name, - direction=leg_direction, + direction=leg_direction.value, asset_address=asset_address, sub_id=sub_id, price=price, @@ -188,11 +188,11 @@ async def transfer_batch( module_address = self._subaccount._config.contracts.RFQ_MODULE maker_module_data = MakerTransferPositionsModuleData( - global_direction=maker_direction, + global_direction=maker_direction.value, positions=transfer_details, ) taker_module_data = TakerTransferPositionsModuleData( - global_direction=taker_direction, + global_direction=taker_direction.value, positions=transfer_details, ) diff --git a/derive_client/_clients/rest/async_http/rfq.py b/derive_client/_clients/rest/async_http/rfq.py index 29b8de66..b6e1f6b0 100644 --- a/derive_client/_clients/rest/async_http/rfq.py +++ b/derive_client/_clients/rest/async_http/rfq.py @@ -182,7 +182,7 @@ async def send_quote( rfq_quote_details = RFQQuoteDetails( instrument_name=leg.instrument_name, - direction=leg.direction, + direction=leg.direction.value, asset_address=asset_address, sub_id=sub_id, price=leg.price, @@ -191,7 +191,7 @@ async def send_quote( rfq_legs.append(rfq_quote_details) module_data = RFQQuoteModuleData( - global_direction=direction, + global_direction=direction.value, max_fee=max_fee, legs=rfq_legs, ) @@ -322,7 +322,7 @@ async def execute_quote( rfq_quote_details = RFQQuoteDetails( instrument_name=leg.instrument_name, - direction=leg.direction, + direction=leg.direction.value, asset_address=asset_address, sub_id=sub_id, price=leg.price, @@ -331,7 +331,7 @@ async def execute_quote( quote_legs.append(rfq_quote_details) module_data = RFQExecuteModuleData( - global_direction=direction, + global_direction=direction.value, max_fee=max_fee, legs=quote_legs, ) diff --git a/derive_client/_clients/rest/async_http/session.py b/derive_client/_clients/rest/async_http/session.py index ce6dfab1..9c17a854 100644 --- a/derive_client/_clients/rest/async_http/session.py +++ b/derive_client/_clients/rest/async_http/session.py @@ -23,15 +23,15 @@ def __init__(self, request_timeout: float, logger: Logger): self._lock = asyncio.Lock() self._finalizer = weakref.finalize(self, self._finalize) - async def open(self) -> None: + async def open(self) -> aiohttp.ClientSession: """Explicit session creation.""" if self._aiohttp_session and not self._aiohttp_session.closed: - return + return self._aiohttp_session async with self._lock: if self._aiohttp_session and not self._aiohttp_session.closed: - return + return self._aiohttp_session self._connector = aiohttp.TCPConnector( limit=100, @@ -41,6 +41,7 @@ async def open(self) -> None: ) self._aiohttp_session = aiohttp.ClientSession(connector=self._connector) + return self._aiohttp_session async def close(self): """Explicit cleanup""" @@ -70,7 +71,7 @@ async def _send_request( *, headers: dict | None = None, ) -> bytes: - await self.open() + session = await self.open() headers = headers or PUBLIC_HEADERS total = _request_timeout_override.get() or self._request_timeout @@ -78,7 +79,7 @@ async def _send_request( timeout = aiohttp.ClientTimeout(total=total) try: - async with self._aiohttp_session.post(url, data=data, headers=headers, timeout=timeout) as response: + async with session.post(url, data=data, headers=headers, timeout=timeout) as response: response.raise_for_status() try: return await response.read() diff --git a/derive_client/_clients/rest/async_http/subaccount.py b/derive_client/_clients/rest/async_http/subaccount.py index f1cce2da..426cd1cc 100644 --- a/derive_client/_clients/rest/async_http/subaccount.py +++ b/derive_client/_clients/rest/async_http/subaccount.py @@ -173,7 +173,7 @@ def mmp(self) -> MMPOperations: def sign_action( self, *, - module_address: Address, + module_address: Address | str, module_data: ModuleData, signature_expiry_sec: Optional[int] = None, nonce: Optional[int] | None = None, diff --git a/derive_client/_clients/rest/async_http/transactions.py b/derive_client/_clients/rest/async_http/transactions.py index 97a822c4..bdf4d67f 100644 --- a/derive_client/_clients/rest/async_http/transactions.py +++ b/derive_client/_clients/rest/async_http/transactions.py @@ -57,7 +57,8 @@ async def deposit_to_subaccount( module_address = self._subaccount._config.contracts.DEPOSIT_MODULE currency = await self._subaccount.markets.get_currency(currency=asset_name) - underlying_address = currency.protocol_asset_addresses.spot + if (asset := currency.protocol_asset_addresses.spot) is None: + raise ValueError(f"asset '{asset_name}' has no spot address, found: {currency}") managers = [] for manager in currency.managers: @@ -74,8 +75,8 @@ async def deposit_to_subaccount( decimals = CURRENCY_DECIMALS[Currency[currency.currency]] module_data = DepositModuleData( - amount=str(amount), - asset=underlying_address, + amount=amount, + asset=asset, manager=manager_address, decimals=decimals, asset_name=asset_name, @@ -116,13 +117,14 @@ async def withdraw_from_subaccount( module_address = self._subaccount._config.contracts.WITHDRAWAL_MODULE currency = await self._subaccount.markets.get_currency(currency=asset_name) + if (asset := currency.protocol_asset_addresses.spot) is None: + raise ValueError(f"asset '{asset_name}' has no spot address, found: {currency}") - underlying_address = currency.protocol_asset_addresses.spot decimals = CURRENCY_DECIMALS[Currency[currency.currency]] module_data = WithdrawModuleData( - amount=str(amount), - asset=underlying_address, + amount=amount, + asset=asset, decimals=decimals, asset_name=asset_name, ) diff --git a/derive_client/_clients/rest/http/account.py b/derive_client/_clients/rest/http/account.py index 60e2680b..ca1ececb 100644 --- a/derive_client/_clients/rest/http/account.py +++ b/derive_client/_clients/rest/http/account.py @@ -104,10 +104,11 @@ def from_api( session_keys_response = private_api.session_keys(session_keys_params) valid_signers = {key.public_session_key: key for key in session_keys_response.result.public_session_keys} - if auth.account.address not in valid_signers: - logger.warning(f"Session key {auth.account.address} is not registered for wallet {auth.wallet}") + signer_address = auth.account.address # type: ignore[attr-defined] + if signer_address not in valid_signers: + logger.warning(f"Session key {signer_address} is not registered for wallet {auth.wallet}") else: - logger.debug(f"Session key validated: {auth.account.address}") + logger.debug(f"Session key validated: {signer_address}") return cls( auth=auth, @@ -118,11 +119,26 @@ def from_api( _state=state, ) + @property + def state(self) -> PrivateGetAccountResultSchema: + """Current mutable state.""" + if not self._state: + msg = "Account state not loaded. Use Account.from_api() to instantiate or call refresh() to load state." + raise RuntimeError(msg) + return self._state + @property def address(self) -> Address: """LightAccount wallet address.""" return self._auth.wallet + def refresh(self) -> LightAccount: + """Refresh mutable state from API.""" + params = PrivateGetAccountParamsSchema(wallet=self._auth.wallet) + response = self._private_api.get_account(params) + self._state = response.result + return self + def build_register_session_key_tx( self, *, @@ -197,7 +213,7 @@ def register_scoped_session_key( public_session_key: str, ip_whitelist: Optional[list[str]] = None, label: Optional[str] = None, - scope: Scope = 'read_only', + scope: Scope = Scope.read_only, signed_raw_tx: Optional[str] = None, ) -> PrivateRegisterScopedSessionKeyResultSchema: params = PrivateRegisterScopedSessionKeyParamsSchema( @@ -267,7 +283,6 @@ def create_subaccount( if margin_type == MarginType.SM and currency is not None: raise ValueError("base_currency must not be provided for standard-margin (SM) subaccounts.") - nonce = nonce if nonce is not None else self._auth.nonce_generator.next() subaccount_id = 0 # must be zero for new account creation module_address = self._config.contracts.DEPOSIT_MODULE @@ -276,7 +291,7 @@ def create_subaccount( asset = self._config.contracts.CASH_ASSET module_data = DepositModuleData( - amount=str(amount), + amount=amount, asset=asset, manager=manager_address, decimals=decimals, @@ -284,7 +299,6 @@ def create_subaccount( ) signed_action = self._auth.sign_action( - nonce=nonce, module_address=module_address, module_data=module_data, signature_expiry_sec=signature_expiry_sec, diff --git a/derive_client/_clients/rest/http/client.py b/derive_client/_clients/rest/http/client.py index f541b453..424dca23 100644 --- a/derive_client/_clients/rest/http/client.py +++ b/derive_client/_clients/rest/http/client.py @@ -4,7 +4,7 @@ from logging import Logger from typing import Generator -from pydantic import validate_call +from pydantic import ConfigDict, validate_call from web3 import Web3 from derive_client._bridge.client import BridgeClient @@ -28,7 +28,7 @@ class HTTPClient: """Synchronous HTTP client""" - @validate_call(config=dict(arbitrary_types_allowed=True)) + @validate_call(config=ConfigDict(arbitrary_types_allowed=True)) def __init__( self, *, @@ -73,10 +73,10 @@ def connect(self) -> None: self._session.open() - self._instantiate_account() + self._light_account = self._instantiate_account() self._markets.fetch_all_instruments(expired=False) - subaccount_ids = self._light_account._state.subaccount_ids + subaccount_ids = self._light_account.state.subaccount_ids if self._subaccount_id not in subaccount_ids: self._logger.warning( f"Subaccount {self._subaccount_id} does not exist for wallet {self._light_account.address}. " @@ -97,8 +97,8 @@ def disconnect(self) -> None: self._markets._perp_instruments_cache.clear() self._markets._option_instruments_cache.clear() - def _instantiate_account(self) -> None: - self._light_account = LightAccount.from_api( + def _instantiate_account(self) -> LightAccount: + return LightAccount.from_api( auth=self._auth, config=self._config, logger=self._logger, @@ -117,26 +117,27 @@ def _instantiate_subaccount(self, subaccount_id: int) -> Subaccount: private_api=self._private_api, ) - def _initialize_bridge(self) -> None: + def _initialize_bridge(self) -> BridgeClient: """Initialize bridge client lazily.""" if self._env is not Environment.PROD: raise NotConnectedError("Bridge module unavailable in non-prod environment.") try: - self._bridge_client = BridgeClient( + bridge_client = BridgeClient( env=self._env, account=self._auth.account, wallet=self._auth.wallet, logger=self._logger, ) - self._bridge_client.connect() + bridge_client.connect() + return bridge_client except BridgePrimarySignerRequiredError: raise NotConnectedError("Bridge unavailable: requires signer to be the LightAccount owner.") @property def account(self) -> LightAccount: if self._light_account is None: - self._instantiate_account() + self._light_account = self._instantiate_account() return self._light_account @property @@ -148,7 +149,7 @@ def active_subaccount(self) -> Subaccount: @property def bridge(self) -> BridgeClient: if not self._bridge_client: - self._initialize_bridge() + self._bridge_client = self._initialize_bridge() return self._bridge_client def fetch_subaccount(self, subaccount_id: int) -> Subaccount: diff --git a/derive_client/_clients/rest/http/positions.py b/derive_client/_clients/rest/http/positions.py index 07fa0c72..5a955a42 100644 --- a/derive_client/_clients/rest/http/positions.py +++ b/derive_client/_clients/rest/http/positions.py @@ -105,7 +105,7 @@ def transfer( maker_params = TradeModuleParamsSchema( amount=abs(amount), - direction=maker_module_data.get_direction(), + direction=Direction[maker_module_data.get_direction()], instrument_name=instrument_name, limit_price=limit_price, max_fee=max_fee, @@ -117,7 +117,7 @@ def transfer( ) taker_params = TradeModuleParamsSchema( amount=abs(amount), - direction=taker_module_data.get_direction(), + direction=Direction[taker_module_data.get_direction()], instrument_name=instrument_name, limit_price=limit_price, max_fee=max_fee, @@ -174,7 +174,7 @@ def transfer_batch( details = TransferPositionsDetails( instrument_name=instrument_name, - direction=leg_direction, + direction=leg_direction.value, asset_address=asset_address, sub_id=sub_id, price=price, @@ -188,11 +188,11 @@ def transfer_batch( module_address = self._subaccount._config.contracts.RFQ_MODULE maker_module_data = MakerTransferPositionsModuleData( - global_direction=maker_direction, + global_direction=maker_direction.value, positions=transfer_details, ) taker_module_data = TakerTransferPositionsModuleData( - global_direction=taker_direction, + global_direction=taker_direction.value, positions=transfer_details, ) diff --git a/derive_client/_clients/rest/http/rfq.py b/derive_client/_clients/rest/http/rfq.py index 73fb3edd..343b87ff 100644 --- a/derive_client/_clients/rest/http/rfq.py +++ b/derive_client/_clients/rest/http/rfq.py @@ -182,7 +182,7 @@ def send_quote( rfq_quote_details = RFQQuoteDetails( instrument_name=leg.instrument_name, - direction=leg.direction, + direction=leg.direction.value, asset_address=asset_address, sub_id=sub_id, price=leg.price, @@ -191,7 +191,7 @@ def send_quote( rfq_legs.append(rfq_quote_details) module_data = RFQQuoteModuleData( - global_direction=direction, + global_direction=direction.value, max_fee=max_fee, legs=rfq_legs, ) @@ -322,7 +322,7 @@ def execute_quote( rfq_quote_details = RFQQuoteDetails( instrument_name=leg.instrument_name, - direction=leg.direction, + direction=leg.direction.value, asset_address=asset_address, sub_id=sub_id, price=leg.price, @@ -331,7 +331,7 @@ def execute_quote( quote_legs.append(rfq_quote_details) module_data = RFQExecuteModuleData( - global_direction=direction, + global_direction=direction.value, max_fee=max_fee, legs=quote_legs, ) diff --git a/derive_client/_clients/rest/http/session.py b/derive_client/_clients/rest/http/session.py index 4ca921ac..63e1e369 100644 --- a/derive_client/_clients/rest/http/session.py +++ b/derive_client/_clients/rest/http/session.py @@ -17,11 +17,11 @@ def __init__(self, request_timeout: float, logger: Logger): self._requests_session: requests.Session | None = None self._finalizer = weakref.finalize(self, self._finalize) - def open(self): + def open(self) -> requests.Session: """Lazy session creation""" if self._requests_session is not None: - return + return self._requests_session session = requests.Session() @@ -43,6 +43,7 @@ def open(self): session.mount("http://", adapter) self._requests_session = session + return self._requests_session def close(self): """Explicit cleanup""" @@ -60,12 +61,12 @@ def _send_request( *, headers: dict | None = None, ) -> bytes: - self.open() + session = self.open() timeout = self._request_timeout try: - response = self._requests_session.post(url, data=data, headers=headers, timeout=timeout) + response = session.post(url, data=data, headers=headers, timeout=timeout) response.raise_for_status() except requests.RequestException as e: self._logger.error("HTTP request failed: %s -> %s", url, e) diff --git a/derive_client/_clients/rest/http/transactions.py b/derive_client/_clients/rest/http/transactions.py index b97091e6..b665f4eb 100644 --- a/derive_client/_clients/rest/http/transactions.py +++ b/derive_client/_clients/rest/http/transactions.py @@ -57,7 +57,8 @@ def deposit_to_subaccount( module_address = self._subaccount._config.contracts.DEPOSIT_MODULE currency = self._subaccount.markets.get_currency(currency=asset_name) - underlying_address = currency.protocol_asset_addresses.spot + if (asset := currency.protocol_asset_addresses.spot) is None: + raise ValueError(f"asset '{asset_name}' has no spot address, found: {currency}") managers = [] for manager in currency.managers: @@ -74,8 +75,8 @@ def deposit_to_subaccount( decimals = CURRENCY_DECIMALS[Currency[currency.currency]] module_data = DepositModuleData( - amount=str(amount), - asset=underlying_address, + amount=amount, + asset=asset, manager=manager_address, decimals=decimals, asset_name=asset_name, @@ -116,13 +117,14 @@ def withdraw_from_subaccount( module_address = self._subaccount._config.contracts.WITHDRAWAL_MODULE currency = self._subaccount.markets.get_currency(currency=asset_name) + if (asset := currency.protocol_asset_addresses.spot) is None: + raise ValueError(f"asset '{asset_name}' has no spot address, found: {currency}") - underlying_address = currency.protocol_asset_addresses.spot decimals = CURRENCY_DECIMALS[Currency[currency.currency]] module_data = WithdrawModuleData( - amount=str(amount), - asset=underlying_address, + amount=amount, + asset=asset, decimals=decimals, asset_name=asset_name, ) diff --git a/derive_client/_clients/utils.py b/derive_client/_clients/utils.py index e97cbdec..ad39aaba 100644 --- a/derive_client/_clients/utils.py +++ b/derive_client/_clients/utils.py @@ -20,16 +20,18 @@ if TYPE_CHECKING: from derive_client._clients.rest.http.markets import MarketOperations + from derive_client._clients.rest.async_http.markets import MarketOperations as AsyncMarketOperations class HasInstrumentName(Protocol): instrument_name: str -T = TypeVar("T", bound=HasInstrumentName) +StructT = TypeVar("StructT", bound=msgspec.Struct) +InstrumentT = TypeVar("InstrumentT", bound=HasInstrumentName) -def sort_by_instrument_name(items: Iterable[T]) -> list[T]: +def sort_by_instrument_name(items: Iterable[InstrumentT]) -> list[InstrumentT]: """Derive API mandate: 'Legs must be sorted by instrument name'.""" return sorted(items, key=lambda item: item.instrument_name) @@ -110,7 +112,7 @@ def __str__(self): return f"{base} [data={self.rpc_error.data!r}]" if self.rpc_error.data is not None else base -def try_cast_response(response: bytes, response_schema: type[msgspec.Struct]) -> msgspec.Struct: +def try_cast_response(response: bytes, response_schema: type[StructT]) -> StructT: try: return msgspec.json.decode(response, type=response_schema) except msgspec.ValidationError: @@ -204,7 +206,7 @@ def fetch_all_pages_of_instrument_type( async def async_fetch_all_pages_of_instrument_type( - markets: MarketOperations, + markets: AsyncMarketOperations, instrument_type: InstrumentType, expired: bool, ) -> list[InstrumentPublicResponseSchema]: From 0129c0190d777a1573058687dccf83cfd158e648 Mon Sep 17 00:00:00 2001 From: zarathustra Date: Fri, 31 Oct 2025 16:09:31 +0100 Subject: [PATCH 08/22] fix: mypy type-check failures in _clients --- .../_clients/rest/async_http/account.py | 2 +- .../_clients/rest/async_http/orders.py | 2 +- .../_clients/rest/async_http/positions.py | 9 +++--- derive_client/_clients/rest/async_http/rfq.py | 2 +- .../_clients/rest/async_http/transactions.py | 2 +- derive_client/_clients/rest/http/account.py | 2 +- derive_client/_clients/rest/http/orders.py | 2 +- derive_client/_clients/rest/http/positions.py | 9 +++--- derive_client/_clients/rest/http/rfq.py | 2 +- .../_clients/rest/http/transactions.py | 2 +- derive_client/_clients/utils.py | 28 ++++++++----------- 11 files changed, 29 insertions(+), 33 deletions(-) diff --git a/derive_client/_clients/rest/async_http/account.py b/derive_client/_clients/rest/async_http/account.py index 08432b46..d65057c7 100644 --- a/derive_client/_clients/rest/async_http/account.py +++ b/derive_client/_clients/rest/async_http/account.py @@ -6,7 +6,7 @@ from logging import Logger from typing import Optional -from derive_action_signing.module_data import DepositModuleData +from derive_action_signing import DepositModuleData from derive_client._clients.rest.async_http.api import AsyncPrivateAPI, AsyncPublicAPI from derive_client._clients.utils import AuthContext diff --git a/derive_client/_clients/rest/async_http/orders.py b/derive_client/_clients/rest/async_http/orders.py index 75f4d859..d826fa5a 100644 --- a/derive_client/_clients/rest/async_http/orders.py +++ b/derive_client/_clients/rest/async_http/orders.py @@ -5,7 +5,7 @@ from decimal import Decimal from typing import TYPE_CHECKING, Optional -from derive_action_signing.module_data import TradeModuleData +from derive_action_signing import TradeModuleData from derive_client.constants import INT64_MAX from derive_client.data.generated.models import ( diff --git a/derive_client/_clients/rest/async_http/positions.py b/derive_client/_clients/rest/async_http/positions.py index 428fd1df..f323295c 100644 --- a/derive_client/_clients/rest/async_http/positions.py +++ b/derive_client/_clients/rest/async_http/positions.py @@ -3,9 +3,9 @@ from __future__ import annotations from decimal import Decimal -from typing import TYPE_CHECKING, Optional +from typing import List, TYPE_CHECKING, Optional -from derive_action_signing.module_data import ( +from derive_action_signing import ( MakerTransferPositionModuleData, MakerTransferPositionsModuleData, TakerTransferPositionModuleData, @@ -13,7 +13,7 @@ TransferPositionsDetails, ) -from derive_client._clients.utils import PositionTransfer, sort_by_instrument_name +from derive_client._clients.utils import sort_by_instrument_name from derive_client.data.generated.models import ( Direction, LegPricedSchema, @@ -26,6 +26,7 @@ SignedQuoteParamsSchema, TradeModuleParamsSchema, ) +from derive_client.data_types import PositionTransfer if TYPE_CHECKING: from .subaccount import Subaccount @@ -139,7 +140,7 @@ async def transfer( async def transfer_batch( self, *, - positions: list[PositionTransfer], + positions: List[PositionTransfer], direction: Direction, to_subaccount: int, signature_expiry_sec: Optional[int] = None, diff --git a/derive_client/_clients/rest/async_http/rfq.py b/derive_client/_clients/rest/async_http/rfq.py index b6e1f6b0..4b4bc17d 100644 --- a/derive_client/_clients/rest/async_http/rfq.py +++ b/derive_client/_clients/rest/async_http/rfq.py @@ -5,7 +5,7 @@ from decimal import Decimal from typing import TYPE_CHECKING, Optional -from derive_action_signing.module_data import ( +from derive_action_signing import ( RFQExecuteModuleData, RFQQuoteDetails, RFQQuoteModuleData, diff --git a/derive_client/_clients/rest/async_http/transactions.py b/derive_client/_clients/rest/async_http/transactions.py index bdf4d67f..8901e7a9 100644 --- a/derive_client/_clients/rest/async_http/transactions.py +++ b/derive_client/_clients/rest/async_http/transactions.py @@ -5,7 +5,7 @@ from decimal import Decimal from typing import TYPE_CHECKING, Optional -from derive_action_signing.module_data import DepositModuleData, WithdrawModuleData +from derive_action_signing import DepositModuleData, WithdrawModuleData from derive_client.constants import CURRENCY_DECIMALS from derive_client.data.generated.models import ( diff --git a/derive_client/_clients/rest/http/account.py b/derive_client/_clients/rest/http/account.py index ca1ececb..1c6cda9b 100644 --- a/derive_client/_clients/rest/http/account.py +++ b/derive_client/_clients/rest/http/account.py @@ -6,7 +6,7 @@ from logging import Logger from typing import Optional -from derive_action_signing.module_data import DepositModuleData +from derive_action_signing import DepositModuleData from derive_client._clients.rest.http.api import PrivateAPI, PublicAPI from derive_client._clients.utils import AuthContext diff --git a/derive_client/_clients/rest/http/orders.py b/derive_client/_clients/rest/http/orders.py index 0ade8907..9927bf05 100644 --- a/derive_client/_clients/rest/http/orders.py +++ b/derive_client/_clients/rest/http/orders.py @@ -5,7 +5,7 @@ from decimal import Decimal from typing import TYPE_CHECKING, Optional -from derive_action_signing.module_data import TradeModuleData +from derive_action_signing import TradeModuleData from derive_client.constants import INT64_MAX from derive_client.data.generated.models import ( diff --git a/derive_client/_clients/rest/http/positions.py b/derive_client/_clients/rest/http/positions.py index 5a955a42..822a1fa1 100644 --- a/derive_client/_clients/rest/http/positions.py +++ b/derive_client/_clients/rest/http/positions.py @@ -3,9 +3,9 @@ from __future__ import annotations from decimal import Decimal -from typing import TYPE_CHECKING, Optional +from typing import List, TYPE_CHECKING, Optional -from derive_action_signing.module_data import ( +from derive_action_signing import ( MakerTransferPositionModuleData, MakerTransferPositionsModuleData, TakerTransferPositionModuleData, @@ -13,7 +13,7 @@ TransferPositionsDetails, ) -from derive_client._clients.utils import PositionTransfer, sort_by_instrument_name +from derive_client._clients.utils import sort_by_instrument_name from derive_client.data.generated.models import ( Direction, LegPricedSchema, @@ -26,6 +26,7 @@ SignedQuoteParamsSchema, TradeModuleParamsSchema, ) +from derive_client.data_types import PositionTransfer if TYPE_CHECKING: from .subaccount import Subaccount @@ -139,7 +140,7 @@ def transfer( def transfer_batch( self, *, - positions: list[PositionTransfer], + positions: List[PositionTransfer], direction: Direction, to_subaccount: int, signature_expiry_sec: Optional[int] = None, diff --git a/derive_client/_clients/rest/http/rfq.py b/derive_client/_clients/rest/http/rfq.py index 343b87ff..1e6f835b 100644 --- a/derive_client/_clients/rest/http/rfq.py +++ b/derive_client/_clients/rest/http/rfq.py @@ -5,7 +5,7 @@ from decimal import Decimal from typing import TYPE_CHECKING, Optional -from derive_action_signing.module_data import ( +from derive_action_signing import ( RFQExecuteModuleData, RFQQuoteDetails, RFQQuoteModuleData, diff --git a/derive_client/_clients/rest/http/transactions.py b/derive_client/_clients/rest/http/transactions.py index b665f4eb..edf9d7b1 100644 --- a/derive_client/_clients/rest/http/transactions.py +++ b/derive_client/_clients/rest/http/transactions.py @@ -5,7 +5,7 @@ from decimal import Decimal from typing import TYPE_CHECKING, Optional -from derive_action_signing.module_data import DepositModuleData, WithdrawModuleData +from derive_action_signing import DepositModuleData, WithdrawModuleData from derive_client.constants import CURRENCY_DECIMALS from derive_client.data.generated.models import ( diff --git a/derive_client/_clients/utils.py b/derive_client/_clients/utils.py index ad39aaba..1888d68e 100644 --- a/derive_client/_clients/utils.py +++ b/derive_client/_clients/utils.py @@ -3,9 +3,8 @@ import json import time from dataclasses import dataclass -from decimal import Decimal from enum import StrEnum -from typing import TYPE_CHECKING, Iterable, Optional, Protocol, TypeVar +from typing import TYPE_CHECKING, Iterable, Optional, TypeVar import msgspec from derive_action_signing import ModuleData, SignedAction @@ -15,20 +14,23 @@ from web3 import AsyncWeb3, Web3 from derive_client.constants import EnvConfig -from derive_client.data.generated.models import InstrumentPublicResponseSchema, InstrumentType, RPCErrorFormatSchema +from derive_client.data.generated.models import ( + InstrumentPublicResponseSchema, + InstrumentType, + LegPricedSchema, + LegUnpricedSchema, + PositionTransfer, + RPCErrorFormatSchema, +) from derive_client.data_types import Address if TYPE_CHECKING: - from derive_client._clients.rest.http.markets import MarketOperations from derive_client._clients.rest.async_http.markets import MarketOperations as AsyncMarketOperations - - -class HasInstrumentName(Protocol): - instrument_name: str + from derive_client._clients.rest.http.markets import MarketOperations StructT = TypeVar("StructT", bound=msgspec.Struct) -InstrumentT = TypeVar("InstrumentT", bound=HasInstrumentName) +InstrumentT = TypeVar("InstrumentT", LegUnpricedSchema, LegPricedSchema, PositionTransfer) def sort_by_instrument_name(items: Iterable[InstrumentT]) -> list[InstrumentT]: @@ -171,14 +173,6 @@ def encode_json_exclude_none(obj: msgspec.Struct) -> bytes: return msgspec.json.encode(filtered) -@dataclass -class PositionTransfer: - """Position to transfer between subaccounts.""" - - instrument_name: str - amount: Decimal # Can be negative (sign indicates long/short) - - def fetch_all_pages_of_instrument_type( markets: MarketOperations, instrument_type: InstrumentType, From 2144c29580ca86604f60b20144fb388c0f6d4daf Mon Sep 17 00:00:00 2001 From: zarathustra Date: Sat, 1 Nov 2025 18:55:21 +0100 Subject: [PATCH 09/22] fix: scripts/generate-models.py --- scripts/generate-models.py | 119 +++++++++++++++++++++++++++---------- 1 file changed, 86 insertions(+), 33 deletions(-) diff --git a/scripts/generate-models.py b/scripts/generate-models.py index e8c0e148..9aeae189 100644 --- a/scripts/generate-models.py +++ b/scripts/generate-models.py @@ -139,20 +139,61 @@ def _ensure_referral_default(node: ast.ClassDef) -> bool: ast.fix_missing_locations(node) +def update_get_tx_result_schema(node: ast.ClassDef) -> None: + """Update fields annotated in OpenAPI spec as "str", but are deserialized into dict.""" + + for stmt in node.body: + if isinstance(stmt, ast.AnnAssign) and isinstance(stmt.target, ast.Name): + field_name = stmt.target.id + if field_name == "data": + stmt.annotation = ast.Name(id="dict", ctx=ast.Load()) + elif field_name == "error_log" and isinstance(stmt.annotation, ast.Subscript): + stmt.annotation.slice = ast.Name(id="dict", ctx=ast.Load()) + + class OptionalRewriter(ast.NodeTransformer): def visit_ClassDef(self, node: ast.ClassDef) -> ast.AST: self.generic_visit(node) + if node.name in {"PrivateOrderParamsSchema", "PrivateReplaceParamsSchema"}: _ensure_referral_default(node) + + if node.name == "PublicGetTransactionResultSchema": + update_get_tx_result_schema(node) + + if node.name == "TriggerPriceType" and self._is_str_enum(node): + self._add_type_ignore_to_index(node) + if not is_struct_class(node): return node node.body = reorder_fields(node.body) return node + def _is_str_enum(self, node: ast.ClassDef) -> bool: + """Check if class inherits from both str and Enum.""" + base_names = {base.id for base in node.bases if isinstance(base, ast.Name)} + return 'str' in base_names and 'Enum' in base_names + + def _add_type_ignore_to_index(self, node: ast.ClassDef) -> None: + """Add type_comment to index assignment to suppress mypy error.""" + for stmt in node.body: + if isinstance(stmt, ast.Assign): + for target in stmt.targets: + if isinstance(target, ast.Name) and target.id == 'index': + # Add type_comment which ast.unparse will render as # type: ignore + stmt.type_comment = 'ignore[assignment]' + return + def patch_code(src: str) -> str: tree = ast.parse(src) + tracker = EnumTracker() + tracker.visit(tree) + + fixer = DefaultValueFixer(tracker.enum_types, tracker.custom_types) + tree = fixer.visit(tree) tree = OptionalRewriter().visit(tree) + ast.fix_missing_locations(tree) return ast.unparse(tree) @@ -176,58 +217,70 @@ def is_struct_class(node: ast.ClassDef) -> bool: return False -def inject_transaction_structs(path: Path, template_path: Path) -> None: - """Inject transaction struct definitions and fix PublicGetTransactionResultSchema.""" +class EnumTracker(ast.NodeVisitor): + """Track which imported names are Enums.""" - print("Injecting transaction structs via AST") + def __init__(self): + self.enum_types: set[str] = set() + self.custom_types: set[str] = {'Decimal'} # Known custom types - template_code = template_path.read_text() - template_tree = ast.parse(template_code) + def visit_ClassDef(self, node: ast.ClassDef) -> None: + # Check if class inherits from Enum or StrEnum + for base in node.bases: + if isinstance(base, ast.Name) and base.id in {'Enum', 'StrEnum', 'IntEnum'}: + self.enum_types.add(node.name) + break + self.generic_visit(node) - struct_classes = [node for node in template_tree.body if isinstance(node, ast.ClassDef)] + def visit_ImportFrom(self, node: ast.ImportFrom) -> None: + # Track Decimal imports + if node.module == 'decimal': + for alias in node.names: + if alias.name == 'Decimal': + self.custom_types.add('Decimal') - code = path.read_text() - tree = ast.parse(code) - target_class_idx = None - target_class = None - for idx, node in enumerate(tree.body): - if isinstance(node, ast.ClassDef) and node.name == "PublicGetTransactionResultSchema": - target_class_idx = idx - target_class = node - break +class DefaultValueFixer(ast.NodeTransformer): + """Fix default values that need type coercion.""" - if target_class_idx is None: - print("Warning: PublicGetTransactionResultSchema not found, skipping injection") - return + def __init__(self, enum_types: set[str], custom_types: set[str]): + self.enum_types = enum_types + self.custom_types = custom_types - for struct_class in reversed(struct_classes): - tree.body.insert(target_class_idx, struct_class) + def visit_AnnAssign(self, node: ast.AnnAssign) -> ast.AST: + if not isinstance(node.target, ast.Name) or node.value is None: + return node - for stmt in target_class.body: - if isinstance(stmt, ast.AnnAssign) and isinstance(stmt.target, ast.Name): - field_name = stmt.target.id + type_name = self._get_type_name(node.annotation) + if not type_name or not isinstance(node.value, ast.Constant): + return node - if field_name == "data": - stmt.annotation = ast.Name(id="TransactionData", ctx=ast.Load()) + if node.value.value is None: + return node - elif field_name == "error_log" and isinstance(stmt.annotation, ast.Subscript): - stmt.annotation.slice = ast.Name(id="TransactionErrorLog", ctx=ast.Load()) + # Fix known custom types that need constructor calls + if type_name in self.custom_types or type_name in self.enum_types: + node.value = ast.Call(func=ast.Name(id=type_name, ctx=ast.Load()), args=[node.value], keywords=[]) - ast.fix_missing_locations(tree) - new_code = ast.unparse(tree) - path.write_text(CUSTOM_HEADER + "\n" + new_code) + return node + + def _get_type_name(self, annotation: ast.expr) -> str | None: + """Extract the main type name from an annotation.""" + if isinstance(annotation, ast.Name): + return annotation.id + elif isinstance(annotation, ast.Subscript): + # Handle Optional[X] -> X + return self._get_type_name(annotation.slice) + return None if __name__ == "__main__": base_url = "https://docs.derive.xyz" repo_root = Path(__file__).parent.parent input_path = repo_root / "openapi-spec.json" - output_path = repo_root / "derive_client" / "data" / "generated" / "models.py" - transaction_structs_template = repo_root / "derive_client" / "data" / "templates" / "transaction_structs.py" + output_path = repo_root / "derive_client" / "data_types" / "generated_models.py" generate_models(input_path=input_path, output_path=output_path) patch_pagination_to_optional(output_path) - inject_transaction_structs(output_path, transaction_structs_template) patch_file(output_path) print("Done.") From cc3ab06173722fcf8ef7cf28eb4324dbd84a8330 Mon Sep 17 00:00:00 2001 From: zarathustra Date: Sat, 1 Nov 2025 18:59:48 +0100 Subject: [PATCH 10/22] fix: data_types and constants --- derive_client/constants.py | 105 +- derive_client/data_types/__init__.py | 78 +- derive_client/data_types/enums.py | 173 +- derive_client/data_types/generated_models.py | 2622 ++++++++++++++++++ derive_client/data_types/models.py | 592 ++-- derive_client/data_types/utils.py | 24 + 6 files changed, 3002 insertions(+), 592 deletions(-) create mode 100644 derive_client/data_types/generated_models.py create mode 100644 derive_client/data_types/utils.py diff --git a/derive_client/constants.py b/derive_client/constants.py index 997b1279..aaa8a3a9 100644 --- a/derive_client/constants.py +++ b/derive_client/constants.py @@ -2,12 +2,13 @@ Constants for Derive (formerly Lyra). """ +from enum import Enum, IntEnum from pathlib import Path from typing import Final from pydantic import BaseModel -from derive_client.data_types import Currency, Environment, UnderlyingCurrency +from derive_client.data_types import ChecksumAddress, Currency, Environment, UnderlyingCurrency INT32_MAX: Final[int] = (1 << 31) - 1 UINT32_MAX: Final[int] = (1 << 32) - 1 @@ -16,20 +17,20 @@ class ContractAddresses(BaseModel, frozen=True): - ETH_PERP: str - BTC_PERP: str - ETH_OPTION: str - BTC_OPTION: str - TRADE_MODULE: str - RFQ_MODULE: str - STANDARD_RISK_MANAGER: str - BTC_PORTFOLIO_RISK_MANAGER: str - ETH_PORTFOLIO_RISK_MANAGER: str - CASH_ASSET: str - USDC_ASSET: str - DEPOSIT_MODULE: str - WITHDRAWAL_MODULE: str - TRANSFER_MODULE: str + ETH_PERP: ChecksumAddress + BTC_PERP: ChecksumAddress + ETH_OPTION: ChecksumAddress + BTC_OPTION: ChecksumAddress + TRADE_MODULE: ChecksumAddress + RFQ_MODULE: ChecksumAddress + STANDARD_RISK_MANAGER: ChecksumAddress + BTC_PORTFOLIO_RISK_MANAGER: ChecksumAddress + ETH_PORTFOLIO_RISK_MANAGER: ChecksumAddress + CASH_ASSET: ChecksumAddress + USDC_ASSET: ChecksumAddress + DEPOSIT_MODULE: ChecksumAddress + WITHDRAWAL_MODULE: ChecksumAddress + TRANSFER_MODULE: ChecksumAddress def __getitem__(self, key): return getattr(self, key) @@ -106,6 +107,60 @@ class EnvConfig(BaseModel, frozen=True): } +class ChainID(IntEnum): + ETH = 1 + OPTIMISM = 10 + DERIVE = LYRA = 957 + BASE = 8453 + MODE = 34443 + ARBITRUM = 42161 + BLAST = 81457 + + @classmethod + def _missing_(cls, value): + try: + int_value = int(value) + return next(member for member in cls if member == int_value) + except (ValueError, TypeError, StopIteration): + return super()._missing_(value) + + +class LayerZeroChainIDv2(IntEnum): + # https://docs.layerzero.network/v2/deployments/deployed-contracts + ETH = 30101 + ARBITRUM = 30110 + OPTIMISM = 30111 + BASE = 30184 + DERIVE = 30311 + + +class SocketAddress(Enum): + ETH = ChecksumAddress("0x943ac2775928318653e91d350574436a1b9b16f9") + ARBITRUM = ChecksumAddress("0x37cc674582049b579571e2ffd890a4d99355f6ba") + OPTIMISM = ChecksumAddress("0x301bD265F0b3C16A58CbDb886Ad87842E3A1c0a4") + BASE = ChecksumAddress("0x12E6e58864cE4402cF2B4B8a8E9c75eAD7280156") + DERIVE = ChecksumAddress("0x565810cbfa3Cf1390963E5aFa2fB953795686339") + + +class DeriveTokenAddress(Enum): + # https://www.coingecko.com/en/coins/derive + + # impl: 0x4909ad99441ea5311b90a94650c394cea4a881b8 (Derive) + ETH = ChecksumAddress("0xb1d1eae60eea9525032a6dcb4c1ce336a1de71be") + + # impl: 0x1eda1f6e04ae37255067c064ae783349cf10bdc5 (DeriveL2) + OPTIMISM = ChecksumAddress("0x33800de7e817a70a694f31476313a7c572bba100") + + # impl: 0x01259207a40925b794c8ac320456f7f6c8fe2636 (DeriveL2) + BASE = ChecksumAddress("0x9d0e8f5b25384c7310cb8c6ae32c8fbeb645d083") + + # impl: 0x5d22b63d83a9be5e054df0e3882592ceffcef097 (DeriveL2) + ARBITRUM = ChecksumAddress("0x77b7787a09818502305c95d68a2571f090abb135") + + # impl: 0x340B51Cb46DBF63B55deD80a78a40aa75Dd4ceDF (DeriveL2) + DERIVE = ChecksumAddress("0x2EE0fd70756EDC663AcC9676658A1497C247693A") + + DEFAULT_REFERER = "0x9135BA0f495244dc0A5F029b25CDE95157Db89AD" GAS_FEE_BUFFER = 1.1 # buffer multiplier to pad maxFeePerGas @@ -187,13 +242,13 @@ class EnvConfig(BaseModel, frozen=True): # Contracts used in bridging module -LYRA_OFT_WITHDRAW_WRAPPER_ADDRESS = "0x9400cc156dad38a716047a67c897973A29A06710" -L1_CHUG_SPLASH_PROXY = "0x61e44dc0dae6888b5a301887732217d5725b0bff" -RESOLVED_DELEGATE_PROXY = "0x5456f02c08e9A018E42C39b351328E5AA864174A" -L2_STANDARD_BRIDGE_PROXY = "0x4200000000000000000000000000000000000010" -L2_CROSS_DOMAIN_MESSENGER_PROXY = "0x4200000000000000000000000000000000000007" -WITHDRAW_WRAPPER_V2 = "0xea8E683D8C46ff05B871822a00461995F93df800" -ETH_DEPOSIT_WRAPPER = "0x46e75B6983126896227a5717f2484efb04A0c151" -BASE_DEPOSIT_WRAPPER = "0x9628bba16db41ea7fe1fd84f9ce53bc27c63f59b" -ARBITRUM_DEPOSIT_WRAPPER = "0x076BB6117750e80AD570D98891B68da86C203A88" -OPTIMISM_DEPOSIT_WRAPPER = "0xC65005131Cfdf06622b99E8E17f72Cf694b586cC" +LYRA_OFT_WITHDRAW_WRAPPER_ADDRESS = ChecksumAddress("0x9400cc156dad38a716047a67c897973A29A06710") +L1_CHUG_SPLASH_PROXY = ChecksumAddress("0x61e44dc0dae6888b5a301887732217d5725b0bff") +RESOLVED_DELEGATE_PROXY = ChecksumAddress("0x5456f02c08e9A018E42C39b351328E5AA864174A") +L2_STANDARD_BRIDGE_PROXY = ChecksumAddress("0x4200000000000000000000000000000000000010") +L2_CROSS_DOMAIN_MESSENGER_PROXY = ChecksumAddress("0x4200000000000000000000000000000000000007") +WITHDRAW_WRAPPER_V2 = ChecksumAddress("0xea8E683D8C46ff05B871822a00461995F93df800") +ETH_DEPOSIT_WRAPPER = ChecksumAddress("0x46e75B6983126896227a5717f2484efb04A0c151") +BASE_DEPOSIT_WRAPPER = ChecksumAddress("0x9628bba16db41ea7fe1fd84f9ce53bc27c63f59b") +ARBITRUM_DEPOSIT_WRAPPER = ChecksumAddress("0x076BB6117750e80AD570D98891B68da86C203A88") +OPTIMISM_DEPOSIT_WRAPPER = ChecksumAddress("0xC65005131Cfdf06622b99E8E17f72Cf694b586cC") diff --git a/derive_client/data_types/__init__.py b/derive_client/data_types/__init__.py index 245165d7..afb2b1b8 100644 --- a/derive_client/data_types/__init__.py +++ b/derive_client/data_types/__init__.py @@ -1,113 +1,79 @@ """Enums and Models used in the derive_client module""" from .enums import ( - ActionType, + BridgeDirection, BridgeType, - ChainID, - CollateralAsset, Currency, DeriveJSONRPCErrorCode, - DeriveTokenAddresses, - DeriveTxStatus, - Direction, Environment, EthereumJSONRPCErrorCode, GasPriority, - InstrumentType, - LayerZeroChainIDv2, - MainnetCurrency, - MarginType, - OrderSide, - OrderStatus, - OrderType, - RfqStatus, - SocketAddress, - SubaccountType, - TimeInForce, TxStatus, UnderlyingCurrency, ) +from .generated_models import ( + Direction, + InstrumentType, + OrderType, +) from .models import ( - Address, BridgeContext, BridgeTxDetails, BridgeTxResult, - CreateSubAccountData, - CreateSubAccountDetails, - DepositResult, + ChecksumAddress, DeriveAddresses, - DeriveTxResult, FeeEstimate, FeeEstimates, FeeHistory, - Leg, - ManagerAddress, MintableTokenData, NonMintableTokenData, - PositionSpec, - PositionsTransfer, PositionTransfer, PreparedBridgeTx, - PSignedTransaction, RPCEndpoints, - SessionKey, - TransferPosition, + TxHash, TxResult, + TypedFilterParams, + TypedLogReceipt, + TypedSignedTransaction, + TypedTransaction, + TypedTxReceipt, Wei, - WithdrawResult, ) __all__ = [ + "ChecksumAddress", "TxStatus", - "DeriveTxStatus", "Direction", + "BridgeDirection", "BridgeType", "BridgeContext", "BridgeTxResult", "TxResult", - "ChainID", - "LayerZeroChainIDv2", - "Leg", - "DeriveTokenAddresses", "Currency", "InstrumentType", "EthereumJSONRPCErrorCode", "DeriveJSONRPCErrorCode", "UnderlyingCurrency", - "OrderSide", "OrderType", - "OrderStatus", - "TimeInForce", "Environment", - "SubaccountType", - "CollateralAsset", - "ActionType", "GasPriority", "FeeHistory", "FeeEstimate", "FeeEstimates", - "RfqStatus", - "Address", - "SessionKey", "MintableTokenData", "NonMintableTokenData", "DeriveAddresses", "CreateSubAccountDetails", "CreateSubAccountData", - "MainnetCurrency", - "MarginType", - "ManagerAddress", - "DepositResult", - "WithdrawResult", - "DeriveTxResult", - "SocketAddress", "RPCEndpoints", - "TransferPosition", + "PositionTransfer", "BridgeTxDetails", - "PositionSpec", "PreparedBridgeTx", - "PSignedTransaction", + "TxHash", + "TypedFilterParams", + "TypedLogReceipt", + "TypedSignedTransaction", + "TypedTransaction", + "TypedTxReceipt", "Wei", - "PositionTransfer", - "PositionsTransfer", ] diff --git a/derive_client/data_types/enums.py b/derive_client/data_types/enums.py index 95b1d475..761051b2 100644 --- a/derive_client/data_types/enums.py +++ b/derive_client/data_types/enums.py @@ -9,22 +9,9 @@ class TxStatus(IntEnum): PENDING = 2 # not yet confirmed, no receipt -class DeriveTxStatus(Enum): - """Status code returned in DeriveClient.get_transaction.""" - - REQUESTED = "requested" - PENDING = "pending" - SETTLED = "settled" - REVERTED = "reverted" - IGNORED = "ignored" - TIMED_OUT = "timed_out" - - -class QuoteStatus(Enum): - OPEN = "open" - FILLED = "filled" - CANCELLED = "cancelled" - EXPIRED = "expired" +class BridgeDirection(StrEnum): + DEPOSIT = "deposit" + WITHDRAW = "withdraw" class BridgeType(Enum): @@ -33,90 +20,12 @@ class BridgeType(Enum): STANDARD = "standard" -class Direction(Enum): - DEPOSIT = "deposit" - WITHDRAW = "withdraw" - - -class ChainID(IntEnum): - ETH = 1 - OPTIMISM = 10 - DERIVE = LYRA = 957 - BASE = 8453 - MODE = 34443 - ARBITRUM = 42161 - BLAST = 81457 - - @classmethod - def _missing_(cls, value): - try: - int_value = int(value) - return next(member for member in cls if member == int_value) - except (ValueError, TypeError, StopIteration): - return super()._missing_(value) - - -class LayerZeroChainIDv2(IntEnum): - # https://docs.layerzero.network/v2/deployments/deployed-contracts - ETH = 30101 - ARBITRUM = 30110 - OPTIMISM = 30111 - BASE = 30184 - DERIVE = 30311 - - class GasPriority(IntEnum): SLOW = 25 MEDIUM = 50 FAST = 75 -class SocketAddress(Enum): - ETH = "0x943ac2775928318653e91d350574436a1b9b16f9" - ARBITRUM = "0x37cc674582049b579571e2ffd890a4d99355f6ba" - OPTIMISM = "0x301bD265F0b3C16A58CbDb886Ad87842E3A1c0a4" - BASE = "0x12E6e58864cE4402cF2B4B8a8E9c75eAD7280156" - DERIVE = "0x565810cbfa3Cf1390963E5aFa2fB953795686339" - - -class DeriveTokenAddresses(Enum): - # https://www.coingecko.com/en/coins/derive - ETH = "0xb1d1eae60eea9525032a6dcb4c1ce336a1de71be" # impl: 0x4909ad99441ea5311b90a94650c394cea4a881b8 (Derive) - OPTIMISM = ( - "0x33800de7e817a70a694f31476313a7c572bba100" # impl: 0x1eda1f6e04ae37255067c064ae783349cf10bdc5 (DeriveL2) - ) - BASE = "0x9d0e8f5b25384c7310cb8c6ae32c8fbeb645d083" # impl: 0x01259207a40925b794c8ac320456f7f6c8fe2636 (DeriveL2) - ARBITRUM = ( - "0x77b7787a09818502305c95d68a2571f090abb135" # impl: 0x5d22b63d83a9be5e054df0e3882592ceffcef097 (DeriveL2) - ) - DERIVE = "0x2EE0fd70756EDC663AcC9676658A1497C247693A" # impl: 0x340B51Cb46DBF63B55deD80a78a40aa75Dd4ceDF (DeriveL2) - - -class SessionKeyScope(Enum): - ADMIN = "admin" - ACCOUNT = "account" - READ_ONLY = "read_only" - - -class MainnetCurrency(Enum): - BTC = "BTC" - ETH = "ETH" - - -class MarginType(Enum): - SM = "SM" - PM = "PM" - PM2 = "PM2" - - -class InstrumentType(Enum): - """Instrument types.""" - - ERC20 = "erc20" - OPTION = "option" - PERP = "perp" - - class UnderlyingCurrency(Enum): """Underlying currencies.""" @@ -170,44 +79,6 @@ class Currency(Enum): SNX = "SNX" -class OrderSide(Enum): - """Order sides.""" - - BUY = "buy" - SELL = "sell" - - -class OrderType(Enum): - """Order types.""" - - LIMIT = "limit" - MARKET = "market" - - -class OrderStatus(Enum): - """Order statuses.""" - - OPEN = "open" - FILLED = "filled" - REJECTED = "rejected" - CANCELLED = "cancelled" - EXPIRED = "expired" - - -class LiquidityRole(Enum): - MAKER = "maker" - TAKER = "taker" - - -class TimeInForce(Enum): - """Time in force.""" - - GTC = "gtc" - IOC = "ioc" - FOK = "fok" - POST_ONLY = "post_only" - - class Environment(Enum): """Environment.""" @@ -215,39 +86,6 @@ class Environment(Enum): TEST = "test" -class SubaccountType(Enum): - """ - Type of sub account - """ - - STANDARD = "standard" - PORTFOLIO = "portfolio" - - -class CollateralAsset(Enum): - """Asset types.""" - - USDC = "usdc" - WEETH = "weeth" - LBTC = "lbtc" - - -class ActionType(Enum): - """Action types.""" - - DEPOSIT = "deposit" - TRANSFER = "transfer" - - -class RfqStatus(Enum): - """RFQ statuses.""" - - OPEN = "open" - FILLED = "filled" - CANCELLED = "cancelled" - EXPIRED = "expired" - - class EthereumJSONRPCErrorCode(IntEnum): # https://ethereum-json-rpc.com/errors PARSE_ERROR = -32700 @@ -382,8 +220,3 @@ class DeriveJSONRPCErrorCode(IntEnum): INVALID_SWELL_SEASON = 18006 VAULT_NOT_FOUND = 18007 MAKER_PROGRAM_NOT_FOUND_19000 = 19000 - - -class OptionType(StrEnum): - CALL = "C" - PUT = "P" diff --git a/derive_client/data_types/generated_models.py b/derive_client/data_types/generated_models.py new file mode 100644 index 00000000..60c55bdb --- /dev/null +++ b/derive_client/data_types/generated_models.py @@ -0,0 +1,2622 @@ +# ruff: noqa: E741 +from __future__ import annotations + +from decimal import Decimal +from enum import Enum +from typing import Any, Dict, List, Optional, Union + +from msgspec import Struct + + +class PublicGetVaultStatisticsParamsSchema(Struct): + pass + + +class VaultStatisticsResponseSchema(Struct): + base_value: Decimal + block_number: int + block_timestamp: int + total_supply: Decimal + usd_tvl: Decimal + usd_value: Decimal + vault_name: str + subaccount_value_at_last_trade: Optional[Decimal] = None + underlying_value: Optional[Decimal] = None + + +class Direction(str, Enum): + buy = 'buy' + sell = 'sell' + + +class LegPricedSchema(Struct): + amount: Decimal + direction: Direction + instrument_name: str + price: Decimal + + +class CancelReason(str, Enum): + field_ = '' + user_request = 'user_request' + insufficient_margin = 'insufficient_margin' + signed_max_fee_too_low = 'signed_max_fee_too_low' + mmp_trigger = 'mmp_trigger' + cancel_on_disconnect = 'cancel_on_disconnect' + session_key_deregistered = 'session_key_deregistered' + subaccount_withdrawn = 'subaccount_withdrawn' + rfq_no_longer_open = 'rfq_no_longer_open' + compliance = 'compliance' + + +class LiquidityRole(str, Enum): + maker = 'maker' + taker = 'taker' + + +class Status(str, Enum): + open = 'open' + filled = 'filled' + cancelled = 'cancelled' + expired = 'expired' + + +class TxStatus(str, Enum): + requested = 'requested' + pending = 'pending' + settled = 'settled' + reverted = 'reverted' + ignored = 'ignored' + timed_out = 'timed_out' + + +class QuoteResultSchema(Struct): + cancel_reason: CancelReason + creation_timestamp: int + direction: Direction + fee: Decimal + fill_pct: Decimal + is_transfer: bool + label: str + last_update_timestamp: int + legs: List[LegPricedSchema] + legs_hash: str + liquidity_role: LiquidityRole + max_fee: Decimal + mmp: bool + nonce: int + quote_id: str + rfq_id: str + signature: str + signature_expiry_sec: int + signer: str + status: Status + subaccount_id: int + tx_hash: Optional[str] = None + tx_status: Optional[TxStatus] = None + + +class PrivateResetMmpParamsSchema(Struct): + subaccount_id: int + currency: Optional[str] = None + + +class Result(str, Enum): + ok = 'ok' + + +class PrivateResetMmpResponseSchema(Struct): + id: Union[str, int] + result: Result + + +class PublicGetOptionSettlementPricesParamsSchema(Struct): + currency: str + + +class ExpiryResponseSchema(Struct): + expiry_date: str + utc_expiry_sec: int + price: Optional[Decimal] = None + + +class TradeModuleParamsSchema(Struct): + amount: Decimal + direction: Direction + instrument_name: str + limit_price: Decimal + max_fee: Decimal + nonce: int + signature: str + signature_expiry_sec: int + signer: str + subaccount_id: int + + +class CancelReason1(str, Enum): + field_ = '' + user_request = 'user_request' + mmp_trigger = 'mmp_trigger' + insufficient_margin = 'insufficient_margin' + signed_max_fee_too_low = 'signed_max_fee_too_low' + cancel_on_disconnect = 'cancel_on_disconnect' + ioc_or_market_partial_fill = 'ioc_or_market_partial_fill' + session_key_deregistered = 'session_key_deregistered' + subaccount_withdrawn = 'subaccount_withdrawn' + compliance = 'compliance' + trigger_failed = 'trigger_failed' + validation_failed = 'validation_failed' + + +class OrderStatus(str, Enum): + open = 'open' + filled = 'filled' + cancelled = 'cancelled' + expired = 'expired' + untriggered = 'untriggered' + + +class OrderType(str, Enum): + limit = 'limit' + market = 'market' + + +class TimeInForce(str, Enum): + gtc = 'gtc' + post_only = 'post_only' + fok = 'fok' + ioc = 'ioc' + + +class TriggerPriceType(str, Enum): + mark = 'mark' + index = 'index' # type: ignore[assignment] + + +class TriggerType(str, Enum): + stoploss = 'stoploss' + takeprofit = 'takeprofit' + + +class OrderResponseSchema(Struct): + amount: Decimal + average_price: Decimal + cancel_reason: CancelReason1 + creation_timestamp: int + direction: Direction + filled_amount: Decimal + instrument_name: str + is_transfer: bool + label: str + last_update_timestamp: int + limit_price: Decimal + max_fee: Decimal + mmp: bool + nonce: int + order_fee: Decimal + order_id: str + order_status: OrderStatus + order_type: OrderType + signature: str + signature_expiry_sec: int + signer: str + subaccount_id: int + time_in_force: TimeInForce + quote_id: Optional[str] = None + replaced_order_id: Optional[str] = None + trigger_price: Optional[Decimal] = None + trigger_price_type: Optional[TriggerPriceType] = None + trigger_reject_message: Optional[str] = None + trigger_type: Optional[TriggerType] = None + + +class TradeResponseSchema(Struct): + direction: Direction + expected_rebate: Decimal + index_price: Decimal + instrument_name: str + is_transfer: bool + label: str + liquidity_role: LiquidityRole + mark_price: Decimal + order_id: str + realized_pnl: Decimal + realized_pnl_excl_fees: Decimal + subaccount_id: int + timestamp: int + trade_amount: Decimal + trade_fee: Decimal + trade_id: str + trade_price: Decimal + transaction_id: str + tx_status: TxStatus + quote_id: Optional[str] = None + tx_hash: Optional[str] = None + + +class PublicDepositDebugParamsSchema(Struct): + amount: Decimal + asset_name: str + nonce: int + signature_expiry_sec: int + signer: str + subaccount_id: int + is_atomic_signing: bool = False + + +class PublicDepositDebugResultSchema(Struct): + action_hash: str + encoded_data: str + encoded_data_hashed: str + typed_data_hash: str + + +class PrivateGetOpenOrdersParamsSchema(Struct): + subaccount_id: int + + +class PrivateGetOpenOrdersResultSchema(Struct): + orders: List[OrderResponseSchema] + subaccount_id: int + + +class SignatureDetailsSchema(Struct): + nonce: int + signature: str + signature_expiry_sec: int + signer: str + + +class TransferDetailsSchema(Struct): + address: str + amount: Decimal + sub_id: int + + +class PrivateTransferErc20ResultSchema(Struct): + status: str + transaction_id: str + + +class Scope(str, Enum): + admin = 'admin' + account = 'account' + read_only = 'read_only' + + +class PrivateRegisterScopedSessionKeyParamsSchema(Struct): + expiry_sec: int + public_session_key: str + wallet: str + ip_whitelist: Optional[List[str]] = None + label: Optional[str] = None + scope: Scope = Scope('read_only') + signed_raw_tx: Optional[str] = None + + +class PrivateRegisterScopedSessionKeyResultSchema(Struct): + expiry_sec: int + public_session_key: str + scope: Scope + ip_whitelist: Optional[List[str]] = None + label: Optional[str] = None + transaction_id: Optional[str] = None + + +class PublicGetCurrencyParamsSchema(PublicGetOptionSettlementPricesParamsSchema): + pass + + +class InstrumentType(str, Enum): + erc20 = 'erc20' + option = 'option' + perp = 'perp' + + +class MarketType(str, Enum): + ALL = 'ALL' + SRM_BASE_ONLY = 'SRM_BASE_ONLY' + SRM_OPTION_ONLY = 'SRM_OPTION_ONLY' + SRM_PERP_ONLY = 'SRM_PERP_ONLY' + CASH = 'CASH' + + +class OpenInterestStatsSchema(Struct): + current_open_interest: Decimal + interest_cap: Decimal + manager_currency: Optional[str] = None + + +class MarginType(str, Enum): + PM = 'PM' + SM = 'SM' + PM2 = 'PM2' + + +class ManagerContractResponseSchema(Struct): + address: str + margin_type: MarginType + currency: Optional[str] = None + + +class PM2CollateralDiscountsSchema(Struct): + im_discount: Decimal + manager_currency: str + mm_discount: Decimal + + +class ProtocolAssetAddressesSchema(Struct): + option: Optional[str] = None + perp: Optional[str] = None + spot: Optional[str] = None + underlying_erc20: Optional[str] = None + + +class PrivateLiquidateParamsSchema(Struct): + cash_transfer: Decimal + last_seen_trade_id: int + liquidated_subaccount_id: int + nonce: int + percent_bid: Decimal + price_limit: Decimal + signature: str + signature_expiry_sec: int + signer: str + subaccount_id: int + + +class PrivateLiquidateResultSchema(Struct): + estimated_bid_price: Decimal + estimated_discount_pnl: Decimal + estimated_percent_bid: Decimal + transaction_id: str + + +class PrivateGetSubaccountValueHistoryParamsSchema(Struct): + end_timestamp: int + period: int + start_timestamp: int + subaccount_id: int + + +class SubAccountValueHistoryResponseSchema(Struct): + subaccount_value: Decimal + timestamp: int + + +class PublicGetMakerProgramsParamsSchema(PublicGetVaultStatisticsParamsSchema): + pass + + +class ProgramResponseSchema(Struct): + asset_types: List[str] + currencies: List[str] + end_timestamp: int + min_notional: Decimal + name: str + rewards: Dict[str, Decimal] + start_timestamp: int + + +class PrivateOrderDebugParamsSchema(Struct): + amount: Decimal + direction: Direction + instrument_name: str + limit_price: Decimal + max_fee: Decimal + nonce: int + signature: str + signature_expiry_sec: int + signer: str + subaccount_id: int + is_atomic_signing: Optional[bool] = False + label: str = '' + mmp: bool = False + order_type: OrderType = OrderType('limit') + reduce_only: bool = False + referral_code: str = '' + reject_timestamp: int = 9223372036854776000 + time_in_force: TimeInForce = TimeInForce('gtc') + trigger_price: Optional[Decimal] = None + trigger_price_type: Optional[TriggerPriceType] = None + trigger_type: Optional[TriggerType] = None + + +class TradeModuleDataSchema(Struct): + asset: str + desired_amount: Decimal + is_bid: bool + limit_price: Decimal + recipient_id: int + sub_id: int + trade_id: str + worst_fee: Decimal + + +class PrivateGetInterestHistoryParamsSchema(Struct): + subaccount_id: int + end_timestamp: int = 9223372036854776000 + start_timestamp: int = 0 + + +class InterestPaymentSchema(Struct): + interest: Decimal + timestamp: int + + +class PrivateGetDepositHistoryParamsSchema(PrivateGetInterestHistoryParamsSchema): + pass + + +class DepositSchema(Struct): + amount: Decimal + asset: str + timestamp: int + transaction_id: str + tx_hash: str + tx_status: TxStatus + error_log: Optional[Dict[str, Any]] = None + + +class PrivateGetMmpConfigParamsSchema(PrivateResetMmpParamsSchema): + pass + + +class MMPConfigResultSchema(Struct): + currency: str + is_frozen: bool + mmp_frozen_time: int + mmp_interval: int + mmp_unfreeze_time: int + subaccount_id: int + mmp_amount_limit: Decimal = Decimal('0') + mmp_delta_limit: Decimal = Decimal('0') + + +class PrivateSessionKeysParamsSchema(Struct): + wallet: str + + +class SessionKeyResponseSchema(Struct): + expiry_sec: int + ip_whitelist: List[str] + label: str + public_session_key: str + scope: str + + +class PublicGetInstrumentsParamsSchema(Struct): + currency: str + expired: bool + instrument_type: InstrumentType + + +class ERC20PublicDetailsSchema(Struct): + decimals: int + borrow_index: Decimal = Decimal('1') + supply_index: Decimal = Decimal('1') + underlying_erc20_address: str = '' + + +class OptionType(str, Enum): + C = 'C' + P = 'P' + + +class OptionPublicDetailsSchema(Struct): + expiry: int + index: str + option_type: OptionType + strike: Decimal + settlement_price: Optional[Decimal] = None + + +class PerpPublicDetailsSchema(Struct): + aggregate_funding: Decimal + funding_rate: Decimal + index: str + max_rate_per_hour: Decimal + min_rate_per_hour: Decimal + static_interest_rate: Decimal + + +class PrivateGetAllPortfoliosParamsSchema(PrivateSessionKeysParamsSchema): + pass + + +class CollateralResponseSchema(Struct): + amount: Decimal + amount_step: Decimal + asset_name: str + asset_type: InstrumentType + average_price: Decimal + average_price_excl_fees: Decimal + creation_timestamp: int + cumulative_interest: Decimal + currency: str + delta: Decimal + delta_currency: str + initial_margin: Decimal + maintenance_margin: Decimal + mark_price: Decimal + mark_value: Decimal + open_orders_margin: Decimal + pending_interest: Decimal + realized_pnl: Decimal + realized_pnl_excl_fees: Decimal + total_fees: Decimal + unrealized_pnl: Decimal + unrealized_pnl_excl_fees: Decimal + + +class PositionResponseSchema(Struct): + amount: Decimal + amount_step: Decimal + average_price: Decimal + average_price_excl_fees: Decimal + creation_timestamp: int + cumulative_funding: Decimal + delta: Decimal + gamma: Decimal + index_price: Decimal + initial_margin: Decimal + instrument_name: str + instrument_type: InstrumentType + maintenance_margin: Decimal + mark_price: Decimal + mark_value: Decimal + net_settlements: Decimal + open_orders_margin: Decimal + pending_funding: Decimal + realized_pnl: Decimal + realized_pnl_excl_fees: Decimal + theta: Decimal + total_fees: Decimal + unrealized_pnl: Decimal + unrealized_pnl_excl_fees: Decimal + vega: Decimal + leverage: Optional[Decimal] = None + liquidation_price: Optional[Decimal] = None + + +class PublicGetInstrumentParamsSchema(Struct): + instrument_name: str + + +class PublicGetInstrumentResultSchema(Struct): + amount_step: Decimal + base_asset_address: str + base_asset_sub_id: str + base_currency: str + base_fee: Decimal + fifo_min_allocation: Decimal + instrument_name: str + instrument_type: InstrumentType + is_active: bool + maker_fee_rate: Decimal + maximum_amount: Decimal + minimum_amount: Decimal + pro_rata_amount_step: Decimal + pro_rata_fraction: Decimal + quote_currency: str + scheduled_activation: int + scheduled_deactivation: int + taker_fee_rate: Decimal + tick_size: Decimal + erc20_details: Optional[ERC20PublicDetailsSchema] = None + option_details: Optional[OptionPublicDetailsSchema] = None + perp_details: Optional[PerpPublicDetailsSchema] = None + mark_price_fee_rate_cap: Optional[Decimal] = None + + +class PublicExecuteQuoteDebugParamsSchema(Struct): + direction: Direction + legs: List[LegPricedSchema] + max_fee: Decimal + nonce: int + quote_id: str + rfq_id: str + signature: str + signature_expiry_sec: int + signer: str + subaccount_id: int + label: str = '' + + +class PublicExecuteQuoteDebugResultSchema(Struct): + action_hash: str + encoded_data: str + encoded_data_hashed: str + encoded_legs: str + legs_hash: str + typed_data_hash: str + + +class PrivateGetCollateralsParamsSchema(PrivateGetOpenOrdersParamsSchema): + pass + + +class PrivateGetCollateralsResultSchema(Struct): + collaterals: List[CollateralResponseSchema] + subaccount_id: int + + +class PrivatePollQuotesParamsSchema(Struct): + subaccount_id: int + from_timestamp: int = 0 + page: int = 1 + page_size: int = 100 + quote_id: Optional[str] = None + rfq_id: Optional[str] = None + status: Optional[Status] = None + to_timestamp: int = 18446744073709552000 + + +class PaginationInfoSchema(Struct): + count: int + num_pages: int + + +class QuoteResultPublicSchema(Struct): + cancel_reason: CancelReason + creation_timestamp: int + direction: Direction + fill_pct: Decimal + last_update_timestamp: int + legs: List[LegPricedSchema] + legs_hash: str + liquidity_role: LiquidityRole + quote_id: str + rfq_id: str + status: Status + subaccount_id: int + wallet: str + tx_hash: Optional[str] = None + tx_status: Optional[TxStatus] = None + + +class SimulatedCollateralSchema(Struct): + amount: Decimal + asset_name: str + + +class SimulatedPositionSchema(Struct): + amount: Decimal + instrument_name: str + entry_price: Optional[Decimal] = None + + +class PrivateGetMarginResultSchema(Struct): + is_valid_trade: bool + post_initial_margin: Decimal + post_maintenance_margin: Decimal + pre_initial_margin: Decimal + pre_maintenance_margin: Decimal + subaccount_id: int + + +class PublicBuildRegisterSessionKeyTxParamsSchema(Struct): + expiry_sec: int + public_session_key: str + wallet: str + gas: Optional[int] = None + nonce: Optional[int] = None + + +class PublicBuildRegisterSessionKeyTxResultSchema(Struct): + tx_params: Dict[str, Any] + + +class PrivateCancelTriggerOrderParamsSchema(Struct): + order_id: str + subaccount_id: int + + +class PrivateCancelTriggerOrderResultSchema(OrderResponseSchema): + pass + + +class PrivateGetOrderParamsSchema(PrivateCancelTriggerOrderParamsSchema): + pass + + +class PrivateGetOrderResultSchema(OrderResponseSchema): + pass + + +class PrivateGetWithdrawalHistoryParamsSchema(PrivateGetInterestHistoryParamsSchema): + pass + + +class WithdrawalSchema(Struct): + amount: Decimal + asset: str + timestamp: int + tx_hash: str + tx_status: TxStatus + error_log: Optional[Dict[str, Any]] = None + + +class PublicGetLiveIncidentsParamsSchema(PublicGetVaultStatisticsParamsSchema): + pass + + +class MonitorType(str, Enum): + manual = 'manual' + auto = 'auto' + + +class Severity(str, Enum): + low = 'low' + medium = 'medium' + high = 'high' + + +class IncidentResponseSchema(Struct): + creation_timestamp_sec: int + label: str + message: str + monitor_type: MonitorType + severity: Severity + + +class PrivateGetQuotesParamsSchema(PrivatePollQuotesParamsSchema): + pass + + +class PrivateGetQuotesResultSchema(Struct): + quotes: List[QuoteResultSchema] + pagination: PaginationInfoSchema | None = None + + +class PrivateGetPositionsParamsSchema(PrivateGetOpenOrdersParamsSchema): + pass + + +class PrivateGetPositionsResultSchema(Struct): + positions: List[PositionResponseSchema] + subaccount_id: int + + +class PrivateGetOptionSettlementHistoryParamsSchema(PrivateGetOpenOrdersParamsSchema): + pass + + +class OptionSettlementResponseSchema(Struct): + amount: Decimal + expiry: int + instrument_name: str + option_settlement_pnl: Decimal + option_settlement_pnl_excl_fees: Decimal + settlement_price: Decimal + subaccount_id: int + + +class PublicDeregisterSessionKeyParamsSchema(Struct): + public_session_key: str + signed_raw_tx: str + wallet: str + + +class PublicDeregisterSessionKeyResultSchema(Struct): + public_session_key: str + transaction_id: str + + +class PublicGetVaultShareParamsSchema(Struct): + from_timestamp_sec: int + to_timestamp_sec: int + vault_name: str + page: int = 1 + page_size: int = 100 + + +class VaultShareResponseSchema(Struct): + base_value: Decimal + block_number: int + block_timestamp: int + usd_value: Decimal + underlying_value: Optional[Decimal] = None + + +class PrivateExpiredAndCancelledHistoryParamsSchema(Struct): + end_timestamp: int + expiry: int + start_timestamp: int + subaccount_id: int + wallet: str + + +class PrivateExpiredAndCancelledHistoryResultSchema(Struct): + presigned_urls: List[str] + + +class PrivateEditSessionKeyParamsSchema(Struct): + public_session_key: str + wallet: str + disable: bool = False + ip_whitelist: Optional[List[str]] = None + label: Optional[str] = None + + +class PrivateEditSessionKeyResultSchema(SessionKeyResponseSchema): + pass + + +class PublicGetAllCurrenciesParamsSchema(PublicGetVaultStatisticsParamsSchema): + pass + + +class CurrencyDetailedResponseSchema(Struct): + asset_cap_and_supply_per_manager: Dict[str, Dict[str, List[OpenInterestStatsSchema]]] + borrow_apy: Decimal + currency: str + instrument_types: List[InstrumentType] + managers: List[ManagerContractResponseSchema] + market_type: MarketType + pm2_collateral_discounts: List[PM2CollateralDiscountsSchema] + protocol_asset_addresses: ProtocolAssetAddressesSchema + spot_price: Decimal + srm_im_discount: Decimal + srm_mm_discount: Decimal + supply_apy: Decimal + total_borrow: Decimal + total_supply: Decimal + spot_price_24h: Optional[Decimal] = None + + +class PrivateCancelByLabelParamsSchema(Struct): + label: str + subaccount_id: int + instrument_name: Optional[str] = None + + +class PrivateCancelByLabelResultSchema(Struct): + cancelled_orders: int + + +class PublicWithdrawDebugParamsSchema(PublicDepositDebugParamsSchema): + pass + + +class PublicWithdrawDebugResultSchema(PublicDepositDebugResultSchema): + pass + + +class PublicGetMarginParamsSchema(Struct): + margin_type: MarginType + simulated_collaterals: List[SimulatedCollateralSchema] + simulated_positions: List[SimulatedPositionSchema] + market: Optional[str] = None + simulated_collateral_changes: Optional[List[SimulatedCollateralSchema]] = None + simulated_position_changes: Optional[List[SimulatedPositionSchema]] = None + + +class PublicGetMarginResultSchema(PrivateGetMarginResultSchema): + pass + + +class PrivateGetSubaccountsParamsSchema(PrivateSessionKeysParamsSchema): + pass + + +class PrivateGetSubaccountsResultSchema(Struct): + subaccount_ids: List[int] + wallet: str + + +class PrivatePollRfqsParamsSchema(Struct): + subaccount_id: int + from_timestamp: int = 0 + page: int = 1 + page_size: int = 100 + rfq_id: Optional[str] = None + rfq_subaccount_id: Optional[int] = None + status: Optional[Status] = None + to_timestamp: int = 18446744073709552000 + + +class LegUnpricedSchema(Struct): + amount: Decimal + direction: Direction + instrument_name: str + + +class PrivateWithdrawParamsSchema(Struct): + amount: Decimal + asset_name: str + nonce: int + signature: str + signature_expiry_sec: int + signer: str + subaccount_id: int + is_atomic_signing: bool = False + + +class PrivateWithdrawResultSchema(PrivateTransferErc20ResultSchema): + pass + + +class Status6(str, Enum): + unseen = 'unseen' + seen = 'seen' + hidden = 'hidden' + + +class PrivateUpdateNotificationsParamsSchema(Struct): + notification_ids: List[int] + subaccount_id: int + status: Status6 = Status6('seen') + + +class PrivateUpdateNotificationsResultSchema(Struct): + updated_count: int + + +class PrivateSetCancelOnDisconnectParamsSchema(Struct): + enabled: bool + wallet: str + + +class PrivateSetCancelOnDisconnectResponseSchema(PrivateResetMmpResponseSchema): + pass + + +class PrivateGetTradeHistoryParamsSchema(Struct): + from_timestamp: int = 0 + instrument_name: Optional[str] = None + order_id: Optional[str] = None + page: int = 1 + page_size: int = 100 + quote_id: Optional[str] = None + subaccount_id: Optional[int] = None + to_timestamp: int = 18446744073709552000 + wallet: Optional[str] = None + + +class PrivateGetTradeHistoryResultSchema(Struct): + subaccount_id: int + trades: List[TradeResponseSchema] + pagination: PaginationInfoSchema | None = None + + +class PrivateOrderParamsSchema(PrivateOrderDebugParamsSchema): + referral_code: str = '0x9135BA0f495244dc0A5F029b25CDE95157Db89AD' + pass + + +class PrivateOrderResultSchema(Struct): + order: OrderResponseSchema + trades: List[TradeResponseSchema] + + +class PublicGetInterestRateHistoryParamsSchema(Struct): + from_timestamp_sec: int + to_timestamp_sec: int + page: int = 1 + page_size: int = 100 + + +class InterestRateHistoryResponseSchema(Struct): + block: int + borrow_apy: Decimal + supply_apy: Decimal + timestamp_sec: int + total_borrow: Decimal + total_supply: Decimal + + +class PublicGetOptionSettlementHistoryParamsSchema(Struct): + page: int = 1 + page_size: int = 100 + subaccount_id: Optional[int] = None + + +class PublicGetOptionSettlementHistoryResultSchema(Struct): + settlements: List[OptionSettlementResponseSchema] + pagination: PaginationInfoSchema | None = None + + +class PublicGetMakerProgramScoresParamsSchema(Struct): + epoch_start_timestamp: int + program_name: str + + +class ScoreBreakdownSchema(Struct): + coverage_score: Decimal + holder_boost: Decimal + quality_score: Decimal + total_score: Decimal + volume: Decimal + volume_multiplier: Decimal + wallet: str + + +class PrivateGetOrdersParamsSchema(Struct): + subaccount_id: int + instrument_name: Optional[str] = None + label: Optional[str] = None + page: int = 1 + page_size: int = 100 + status: Optional[OrderStatus] = None + + +class PrivateGetOrdersResultSchema(Struct): + orders: List[OrderResponseSchema] + subaccount_id: int + pagination: PaginationInfoSchema | None = None + + +class PublicGetTickerParamsSchema(PublicGetInstrumentParamsSchema): + pass + + +class OptionPricingSchema(Struct): + ask_iv: Decimal + bid_iv: Decimal + delta: Decimal + discount_factor: Decimal + forward_price: Decimal + gamma: Decimal + iv: Decimal + mark_price: Decimal + rho: Decimal + theta: Decimal + vega: Decimal + + +class AggregateTradingStatsSchema(Struct): + contract_volume: Decimal + high: Decimal + low: Decimal + num_trades: Decimal + open_interest: Decimal + percent_change: Decimal + usd_change: Decimal + + +class PublicLoginParamsSchema(Struct): + signature: str + timestamp: str + wallet: str + + +class PublicLoginResponseSchema(Struct): + id: Union[str, int] + result: List[int] + + +class PrivateGetFundingHistoryParamsSchema(Struct): + subaccount_id: int + end_timestamp: int = 9223372036854776000 + instrument_name: Optional[str] = None + page: int = 1 + page_size: int = 100 + start_timestamp: int = 0 + + +class FundingPaymentSchema(Struct): + funding: Decimal + instrument_name: str + pnl: Decimal + timestamp: int + + +class PublicGetSpotFeedHistoryParamsSchema(Struct): + currency: str + end_timestamp: int + period: int + start_timestamp: int + + +class SpotFeedHistoryResponseSchema(Struct): + price: Decimal + timestamp: int + timestamp_bucket: int + + +class PrivateSetMmpConfigParamsSchema(Struct): + currency: str + mmp_frozen_time: int + mmp_interval: int + subaccount_id: int + mmp_amount_limit: Decimal = Decimal('0') + mmp_delta_limit: Decimal = Decimal('0') + + +class PrivateSetMmpConfigResultSchema(PrivateSetMmpConfigParamsSchema): + pass + + +class Period(str, Enum): + field_900 = 900 + field_3600 = 3600 + field_14400 = 14400 + field_28800 = 28800 + field_86400 = 86400 + + +class PublicGetFundingRateHistoryParamsSchema(Struct): + instrument_name: str + end_timestamp: int = 9223372036854776000 + period: Period = Period(3600) + start_timestamp: int = 0 + + +class FundingRateSchema(Struct): + funding_rate: Decimal + timestamp: int + + +class TypeEnum(str, Enum): + deposit = 'deposit' + withdraw = 'withdraw' + transfer = 'transfer' + trade = 'trade' + settlement = 'settlement' + liquidation = 'liquidation' + custom = 'custom' + + +class PrivateGetNotificationsParamsSchema(Struct): + page: Optional[int] = 1 + page_size: Optional[int] = 50 + status: Optional[Status6] = None + subaccount_id: Optional[int] = None + type: Optional[List[TypeEnum]] = None + wallet: Optional[str] = None + + +class NotificationResponseSchema(Struct): + event: str + event_details: Dict[str, Any] + id: int + status: str + subaccount_id: int + timestamp: int + transaction_id: Optional[int] = None + tx_hash: Optional[str] = None + + +class PrivateCancelBatchQuotesParamsSchema(Struct): + subaccount_id: int + label: Optional[str] = None + nonce: Optional[int] = None + quote_id: Optional[str] = None + rfq_id: Optional[str] = None + + +class PrivateCancelBatchQuotesResultSchema(Struct): + cancelled_ids: List[str] + + +class PrivateRfqGetBestQuoteParamsSchema(Struct): + legs: List[LegUnpricedSchema] + subaccount_id: int + counterparties: Optional[List[str]] = None + direction: Direction = Direction('buy') + label: str = '' + max_total_cost: Optional[Decimal] = None + min_total_cost: Optional[Decimal] = None + partial_fill_step: Decimal = Decimal('1') + rfq_id: Optional[str] = None + + +class InvalidReason(str, Enum): + Account_is_currently_under_maintenance_margin_requirements__trading_is_frozen_ = ( + 'Account is currently under maintenance margin requirements, trading is frozen.' + ) + This_order_would_cause_account_to_fall_under_maintenance_margin_requirements_ = ( + 'This order would cause account to fall under maintenance margin requirements.' + ) + Insufficient_buying_power__only_a_single_risk_reducing_open_order_is_allowed_ = ( + 'Insufficient buying power, only a single risk-reducing open order is allowed.' + ) + Insufficient_buying_power__consider_reducing_order_size_ = ( + 'Insufficient buying power, consider reducing order size.' + ) + Insufficient_buying_power__consider_reducing_order_size_or_canceling_other_orders_ = ( + 'Insufficient buying power, consider reducing order size or canceling other orders.' + ) + Consider_canceling_other_limit_orders_or_using_IOC__FOK__or_market_orders__This_order_is_risk_reducing__but_if_filled_with_other_open_orders__buying_power_might_be_insufficient_ = 'Consider canceling other limit orders or using IOC, FOK, or market orders. This order is risk-reducing, but if filled with other open orders, buying power might be insufficient.' + Insufficient_buying_power_ = 'Insufficient buying power.' + + +class PrivateRfqGetBestQuoteResultSchema(Struct): + direction: Direction + estimated_fee: Decimal + estimated_realized_pnl: Decimal + estimated_realized_pnl_excl_fees: Decimal + estimated_total_cost: Decimal + filled_pct: Decimal + is_valid: bool + post_initial_margin: Decimal + pre_initial_margin: Decimal + suggested_max_fee: Decimal + best_quote: Optional[QuoteResultPublicSchema] = None + down_liquidation_price: Optional[Decimal] = None + invalid_reason: Optional[InvalidReason] = None + post_liquidation_price: Optional[Decimal] = None + up_liquidation_price: Optional[Decimal] = None + + +class PrivateDepositParamsSchema(PrivateWithdrawParamsSchema): + pass + + +class PrivateDepositResultSchema(PrivateTransferErc20ResultSchema): + pass + + +class PublicGetLiquidationHistoryParamsSchema(Struct): + end_timestamp: int = 9223372036854776000 + page: int = 1 + page_size: int = 100 + start_timestamp: int = 0 + subaccount_id: Optional[int] = None + + +class AuctionType(str, Enum): + solvent = 'solvent' + insolvent = 'insolvent' + + +class AuctionBidEventSchema(Struct): + amounts_liquidated: Dict[str, Decimal] + cash_received: Decimal + discount_pnl: Decimal + percent_liquidated: Decimal + positions_realized_pnl: Dict[str, Decimal] + positions_realized_pnl_excl_fees: Dict[str, Decimal] + realized_pnl: Decimal + realized_pnl_excl_fees: Decimal + timestamp: int + tx_hash: str + + +class PrivateChangeSubaccountLabelParamsSchema(Struct): + label: str + subaccount_id: int + + +class PrivateChangeSubaccountLabelResultSchema(PrivateChangeSubaccountLabelParamsSchema): + pass + + +class PublicMarginWatchParamsSchema(Struct): + subaccount_id: int + force_onchain: bool = False + + +class CollateralPublicResponseSchema(Struct): + amount: Decimal + asset_name: str + asset_type: InstrumentType + initial_margin: Decimal + maintenance_margin: Decimal + mark_price: Decimal + mark_value: Decimal + + +class PositionPublicResponseSchema(Struct): + amount: Decimal + delta: Decimal + gamma: Decimal + index_price: Decimal + initial_margin: Decimal + instrument_name: str + instrument_type: InstrumentType + maintenance_margin: Decimal + mark_price: Decimal + mark_value: Decimal + theta: Decimal + vega: Decimal + liquidation_price: Optional[Decimal] = None + + +class PublicGetTransactionParamsSchema(Struct): + transaction_id: str + + +class PublicGetTransactionResultSchema(Struct): + data: dict + status: TxStatus + error_log: Optional[dict] = None + transaction_hash: Optional[str] = None + + +class PrivateGetErc20TransferHistoryParamsSchema(PrivateGetInterestHistoryParamsSchema): + pass + + +class ERC20TransferSchema(Struct): + amount: Decimal + asset: str + counterparty_subaccount_id: int + is_outgoing: bool + timestamp: int + tx_hash: str + + +class PrivateReplaceParamsSchema(Struct): + amount: Decimal + direction: Direction + instrument_name: str + limit_price: Decimal + max_fee: Decimal + nonce: int + signature: str + signature_expiry_sec: int + signer: str + subaccount_id: int + expected_filled_amount: Optional[Decimal] = None + is_atomic_signing: Optional[bool] = False + label: str = '' + mmp: bool = False + nonce_to_cancel: Optional[int] = None + order_id_to_cancel: Optional[str] = None + order_type: OrderType = OrderType('limit') + reduce_only: bool = False + referral_code: str = '0x9135BA0f495244dc0A5F029b25CDE95157Db89AD' + reject_timestamp: int = 9223372036854776000 + time_in_force: TimeInForce = TimeInForce('gtc') + trigger_price: Optional[Decimal] = None + trigger_price_type: Optional[TriggerPriceType] = None + trigger_type: Optional[TriggerType] = None + + +class RPCErrorFormatSchema(Struct): + code: int + message: str + data: Optional[str] = None + + +class TxStatus5(str, Enum): + settled = 'settled' + reverted = 'reverted' + timed_out = 'timed_out' + + +class PublicGetTradeHistoryParamsSchema(Struct): + currency: Optional[str] = None + from_timestamp: int = 0 + instrument_name: Optional[str] = None + instrument_type: Optional[InstrumentType] = None + page: int = 1 + page_size: int = 100 + subaccount_id: Optional[int] = None + to_timestamp: int = 18446744073709552000 + trade_id: Optional[str] = None + tx_hash: Optional[str] = None + tx_status: TxStatus5 = TxStatus5('settled') + + +class TradeSettledPublicResponseSchema(Struct): + direction: Direction + expected_rebate: Decimal + index_price: Decimal + instrument_name: str + liquidity_role: LiquidityRole + mark_price: Decimal + realized_pnl: Decimal + realized_pnl_excl_fees: Decimal + subaccount_id: int + timestamp: int + trade_amount: Decimal + trade_fee: Decimal + trade_id: str + trade_price: Decimal + tx_hash: str + tx_status: TxStatus5 + wallet: str + quote_id: Optional[str] = None + + +class PublicSendQuoteDebugParamsSchema(Struct): + direction: Direction + legs: List[LegPricedSchema] + max_fee: Decimal + nonce: int + rfq_id: str + signature: str + signature_expiry_sec: int + signer: str + subaccount_id: int + label: str = '' + mmp: bool = False + + +class PublicSendQuoteDebugResultSchema(PublicDepositDebugResultSchema): + pass + + +class PrivateGetOrderHistoryParamsSchema(Struct): + subaccount_id: int + page: int = 1 + page_size: int = 100 + + +class PrivateGetOrderHistoryResultSchema(PrivateGetOrdersResultSchema): + pass + + +class PrivateCancelBatchRfqsParamsSchema(Struct): + subaccount_id: int + label: Optional[str] = None + nonce: Optional[int] = None + rfq_id: Optional[str] = None + + +class PrivateCancelBatchRfqsResultSchema(PrivateCancelBatchQuotesResultSchema): + pass + + +class PrivateExecuteQuoteParamsSchema(PublicExecuteQuoteDebugParamsSchema): + pass + + +class PrivateExecuteQuoteResultSchema(Struct): + cancel_reason: CancelReason + creation_timestamp: int + direction: Direction + fee: Decimal + fill_pct: Decimal + is_transfer: bool + label: str + last_update_timestamp: int + legs: List[LegPricedSchema] + legs_hash: str + liquidity_role: LiquidityRole + max_fee: Decimal + mmp: bool + nonce: int + quote_id: str + rfq_filled_pct: Decimal + rfq_id: str + signature: str + signature_expiry_sec: int + signer: str + status: Status + subaccount_id: int + tx_hash: Optional[str] = None + tx_status: Optional[TxStatus] = None + + +class PublicCreateSubaccountDebugParamsSchema(Struct): + amount: Decimal + asset_name: str + margin_type: MarginType + nonce: int + signature_expiry_sec: int + signer: str + wallet: str + currency: Optional[str] = None + + +class PublicCreateSubaccountDebugResultSchema(PublicDepositDebugResultSchema): + pass + + +class PrivateGetLiquidationHistoryParamsSchema(PrivateGetInterestHistoryParamsSchema): + pass + + +class PrivateCancelRfqParamsSchema(Struct): + rfq_id: str + subaccount_id: int + + +class PrivateCancelRfqResponseSchema(PrivateResetMmpResponseSchema): + pass + + +class PublicGetLatestSignedFeedsParamsSchema(Struct): + currency: Optional[str] = None + expiry: Optional[int] = None + + +class OracleSignatureDataSchema(Struct): + signatures: Optional[List[str]] = None + signers: Optional[List[str]] = None + + +class Type(str, Enum): + P = 'P' + A = 'A' + B = 'B' + + +class PerpFeedDataSchema(Struct): + confidence: Decimal + currency: str + deadline: int + signatures: OracleSignatureDataSchema + spot_diff_value: Decimal + timestamp: int + type: Type + + +class RateFeedDataSchema(Struct): + confidence: Decimal + currency: str + deadline: int + expiry: int + rate: Decimal + signatures: OracleSignatureDataSchema + timestamp: int + + +class FeedSourceType(str, Enum): + S = 'S' + O = 'O' + + +class SpotFeedDataSchema(Struct): + confidence: Decimal + currency: str + deadline: int + price: Decimal + signatures: OracleSignatureDataSchema + timestamp: int + feed_source_type: FeedSourceType = FeedSourceType('S') + + +class VolSVIParamDataSchema(Struct): + SVI_a: Decimal + SVI_b: Decimal + SVI_fwd: Decimal + SVI_m: Decimal + SVI_refTau: Decimal + SVI_rho: Decimal + SVI_sigma: Decimal + + +class PublicGetReferralPerformanceParamsSchema(Struct): + end_ms: int + start_ms: int + referral_code: Optional[str] = None + wallet: Optional[str] = None + + +class ReferralPerformanceByInstrumentTypeSchema(Struct): + fee_reward: Decimal + notional_volume: Decimal + referred_fee: Decimal + + +class PrivateCreateSubaccountParamsSchema(Struct): + amount: Decimal + asset_name: str + margin_type: MarginType + nonce: int + signature: str + signature_expiry_sec: int + signer: str + wallet: str + currency: Optional[str] = None + + +class PrivateCreateSubaccountResultSchema(PrivateTransferErc20ResultSchema): + pass + + +class PrivateCancelParamsSchema(Struct): + instrument_name: str + order_id: str + subaccount_id: int + + +class PrivateCancelResultSchema(OrderResponseSchema): + pass + + +class Period1(str, Enum): + field_60 = 60 + field_300 = 300 + field_900 = 900 + field_1800 = 1800 + field_3600 = 3600 + field_14400 = 14400 + field_28800 = 28800 + field_86400 = 86400 + field_604800 = 604800 + + +class PublicGetSpotFeedHistoryCandlesParamsSchema(Struct): + currency: str + end_timestamp: int + period: Period1 + start_timestamp: int + + +class SpotFeedHistoryCandlesResponseSchema(Struct): + close_price: Decimal + high_price: Decimal + low_price: Decimal + open_price: Decimal + price: Decimal + timestamp: int + timestamp_bucket: int + + +class PrivateGetRfqsParamsSchema(Struct): + subaccount_id: int + from_timestamp: int = 0 + page: int = 1 + page_size: int = 100 + rfq_id: Optional[str] = None + status: Optional[Status] = None + to_timestamp: int = 18446744073709552000 + + +class RFQResultSchema(Struct): + cancel_reason: CancelReason + creation_timestamp: int + filled_pct: Decimal + label: str + last_update_timestamp: int + legs: List[LegUnpricedSchema] + partial_fill_step: Decimal + rfq_id: str + status: Status + subaccount_id: int + valid_until: int + ask_total_cost: Optional[Decimal] = None + bid_total_cost: Optional[Decimal] = None + counterparties: Optional[List[str]] = None + filled_direction: Optional[Direction] = None + mark_total_cost: Optional[Decimal] = None + max_total_cost: Optional[Decimal] = None + min_total_cost: Optional[Decimal] = None + total_cost: Optional[Decimal] = None + + +class PrivateCancelByNonceParamsSchema(Struct): + instrument_name: str + nonce: int + subaccount_id: int + wallet: str + + +class PrivateCancelByNonceResultSchema(PrivateCancelByLabelResultSchema): + pass + + +class PrivateGetSubaccountParamsSchema(PrivateGetOpenOrdersParamsSchema): + pass + + +class PrivateSendRfqParamsSchema(Struct): + legs: List[LegUnpricedSchema] + subaccount_id: int + counterparties: Optional[List[str]] = None + label: str = '' + max_total_cost: Optional[Decimal] = None + min_total_cost: Optional[Decimal] = None + partial_fill_step: Decimal = Decimal('1') + + +class PrivateSendRfqResultSchema(RFQResultSchema): + pass + + +class PublicGetTimeParamsSchema(PublicGetVaultStatisticsParamsSchema): + pass + + +class PublicGetTimeResponseSchema(Struct): + id: Union[str, int] + result: int + + +class PublicStatisticsParamsSchema(Struct): + instrument_name: str + currency: Optional[str] = None + end_time: Optional[int] = None + + +class PublicStatisticsResultSchema(Struct): + daily_fees: Decimal + daily_notional_volume: Decimal + daily_premium_volume: Decimal + daily_trades: int + open_interest: Decimal + total_fees: Decimal + total_notional_volume: Decimal + total_premium_volume: Decimal + total_trades: int + + +class PublicGetAllInstrumentsParamsSchema(Struct): + expired: bool + instrument_type: InstrumentType + currency: Optional[str] = None + page: int = 1 + page_size: int = 100 + + +class PrivateGetLiquidatorHistoryParamsSchema(Struct): + subaccount_id: int + end_timestamp: int = 9223372036854776000 + page: int = 1 + page_size: int = 100 + start_timestamp: int = 0 + + +class PrivateGetLiquidatorHistoryResultSchema(Struct): + bids: List[AuctionBidEventSchema] + pagination: PaginationInfoSchema | None = None + + +class PublicRegisterSessionKeyParamsSchema(Struct): + expiry_sec: int + label: str + public_session_key: str + signed_raw_tx: str + wallet: str + + +class PublicRegisterSessionKeyResultSchema(Struct): + label: str + public_session_key: str + transaction_id: str + + +class PublicGetVaultBalancesParamsSchema(Struct): + smart_contract_owner: Optional[str] = None + wallet: Optional[str] = None + + +class VaultBalanceResponseSchema(Struct): + address: str + amount: Decimal + chain_id: int + name: str + vault_asset_type: str + + +class PrivateGetAccountParamsSchema(PrivateSessionKeysParamsSchema): + pass + + +class AccountFeeInfoSchema(Struct): + base_fee_discount: Decimal + rfq_maker_discount: Decimal + rfq_taker_discount: Decimal + option_maker_fee: Optional[Decimal] = None + option_taker_fee: Optional[Decimal] = None + perp_maker_fee: Optional[Decimal] = None + perp_taker_fee: Optional[Decimal] = None + spot_maker_fee: Optional[Decimal] = None + spot_taker_fee: Optional[Decimal] = None + + +class PrivateCancelQuoteParamsSchema(Struct): + quote_id: str + subaccount_id: int + + +class PrivateCancelQuoteResultSchema(QuoteResultSchema): + pass + + +class PrivateCancelByInstrumentParamsSchema(Struct): + instrument_name: str + subaccount_id: int + + +class PrivateCancelByInstrumentResultSchema(PrivateCancelByLabelResultSchema): + pass + + +class PrivateSendQuoteParamsSchema(PublicSendQuoteDebugParamsSchema): + pass + + +class PrivateSendQuoteResultSchema(QuoteResultSchema): + pass + + +class PrivateCancelAllParamsSchema(PrivateGetOpenOrdersParamsSchema): + pass + + +class PrivateCancelAllResponseSchema(PrivateResetMmpResponseSchema): + pass + + +class PublicGetVaultStatisticsResponseSchema(Struct): + id: Union[str, int] + result: List[VaultStatisticsResponseSchema] + + +class SignedQuoteParamsSchema(Struct): + direction: Direction + legs: List[LegPricedSchema] + max_fee: Decimal + nonce: int + signature: str + signature_expiry_sec: int + signer: str + subaccount_id: int + + +class PrivateTransferPositionsResultSchema(Struct): + maker_quote: QuoteResultSchema + taker_quote: QuoteResultSchema + + +class PublicGetOptionSettlementPricesResultSchema(Struct): + expiries: List[ExpiryResponseSchema] + + +class PrivateTransferPositionParamsSchema(Struct): + maker_params: TradeModuleParamsSchema + taker_params: TradeModuleParamsSchema + wallet: str + + +class PrivateTransferPositionResultSchema(Struct): + maker_order: OrderResponseSchema + maker_trade: TradeResponseSchema + taker_order: OrderResponseSchema + taker_trade: TradeResponseSchema + + +class PublicDepositDebugResponseSchema(Struct): + id: Union[str, int] + result: PublicDepositDebugResultSchema + + +class PrivateGetOpenOrdersResponseSchema(Struct): + id: Union[str, int] + result: PrivateGetOpenOrdersResultSchema + + +class PrivateTransferErc20ParamsSchema(Struct): + recipient_details: SignatureDetailsSchema + recipient_subaccount_id: int + sender_details: SignatureDetailsSchema + subaccount_id: int + transfer: TransferDetailsSchema + + +class PrivateTransferErc20ResponseSchema(Struct): + id: Union[str, int] + result: PrivateTransferErc20ResultSchema + + +class PrivateRegisterScopedSessionKeyResponseSchema(Struct): + id: Union[str, int] + result: PrivateRegisterScopedSessionKeyResultSchema + + +class PublicGetCurrencyResultSchema(CurrencyDetailedResponseSchema): + pass + + +class PrivateLiquidateResponseSchema(Struct): + id: Union[str, int] + result: PrivateLiquidateResultSchema + + +class PrivateGetSubaccountValueHistoryResultSchema(Struct): + subaccount_id: int + subaccount_value_history: List[SubAccountValueHistoryResponseSchema] + + +class PublicGetMakerProgramsResponseSchema(Struct): + id: Union[str, int] + result: List[ProgramResponseSchema] + + +class SignedTradeOrderSchema(Struct): + data: TradeModuleDataSchema + expiry: int + is_atomic_signing: bool + module: str + nonce: int + owner: str + signature: str + signer: str + subaccount_id: int + + +class PrivateGetInterestHistoryResultSchema(Struct): + events: List[InterestPaymentSchema] + + +class PrivateGetDepositHistoryResultSchema(Struct): + events: List[DepositSchema] + + +class PrivateGetMmpConfigResponseSchema(Struct): + id: Union[str, int] + result: List[MMPConfigResultSchema] + + +class PrivateSessionKeysResultSchema(Struct): + public_session_keys: List[SessionKeyResponseSchema] + + +class InstrumentPublicResponseSchema(PublicGetInstrumentResultSchema): + pass + + +class PrivateGetSubaccountResultSchema(Struct): + collaterals: List[CollateralResponseSchema] + collaterals_initial_margin: Decimal + collaterals_maintenance_margin: Decimal + collaterals_value: Decimal + currency: str + initial_margin: Decimal + is_under_liquidation: bool + label: str + maintenance_margin: Decimal + margin_type: MarginType + open_orders: List[OrderResponseSchema] + open_orders_margin: Decimal + positions: List[PositionResponseSchema] + positions_initial_margin: Decimal + positions_maintenance_margin: Decimal + positions_value: Decimal + projected_margin_change: Decimal + subaccount_id: int + subaccount_value: Decimal + + +class PublicGetInstrumentResponseSchema(Struct): + id: Union[str, int] + result: PublicGetInstrumentResultSchema + + +class PublicExecuteQuoteDebugResponseSchema(Struct): + id: Union[str, int] + result: PublicExecuteQuoteDebugResultSchema + + +class PrivateGetCollateralsResponseSchema(Struct): + id: Union[str, int] + result: PrivateGetCollateralsResultSchema + + +class PrivatePollQuotesResultSchema(Struct): + quotes: List[QuoteResultPublicSchema] + pagination: PaginationInfoSchema | None = None + + +class PrivateGetMarginParamsSchema(Struct): + subaccount_id: int + simulated_collateral_changes: Optional[List[SimulatedCollateralSchema]] = None + simulated_position_changes: Optional[List[SimulatedPositionSchema]] = None + + +class PrivateGetMarginResponseSchema(Struct): + id: Union[str, int] + result: PrivateGetMarginResultSchema + + +class PublicBuildRegisterSessionKeyTxResponseSchema(Struct): + id: Union[str, int] + result: PublicBuildRegisterSessionKeyTxResultSchema + + +class PrivateCancelTriggerOrderResponseSchema(Struct): + id: Union[str, int] + result: PrivateCancelTriggerOrderResultSchema + + +class PrivateGetOrderResponseSchema(Struct): + id: Union[str, int] + result: PrivateGetOrderResultSchema + + +class PrivateGetWithdrawalHistoryResultSchema(Struct): + events: List[WithdrawalSchema] + + +class PublicGetLiveIncidentsResultSchema(Struct): + incidents: List[IncidentResponseSchema] + + +class PrivateGetQuotesResponseSchema(Struct): + id: Union[str, int] + result: PrivateGetQuotesResultSchema + + +class PrivateGetPositionsResponseSchema(Struct): + id: Union[str, int] + result: PrivateGetPositionsResultSchema + + +class PrivateGetOptionSettlementHistoryResultSchema(Struct): + settlements: List[OptionSettlementResponseSchema] + subaccount_id: int + + +class PublicDeregisterSessionKeyResponseSchema(Struct): + id: Union[str, int] + result: PublicDeregisterSessionKeyResultSchema + + +class PublicGetVaultShareResultSchema(Struct): + vault_shares: List[VaultShareResponseSchema] + pagination: PaginationInfoSchema | None = None + + +class PrivateExpiredAndCancelledHistoryResponseSchema(Struct): + id: Union[str, int] + result: PrivateExpiredAndCancelledHistoryResultSchema + + +class PrivateEditSessionKeyResponseSchema(Struct): + id: Union[str, int] + result: PrivateEditSessionKeyResultSchema + + +class PublicGetAllCurrenciesResponseSchema(Struct): + id: Union[str, int] + result: List[CurrencyDetailedResponseSchema] + + +class PrivateCancelByLabelResponseSchema(Struct): + id: Union[str, int] + result: PrivateCancelByLabelResultSchema + + +class PublicWithdrawDebugResponseSchema(Struct): + id: Union[str, int] + result: PublicWithdrawDebugResultSchema + + +class PublicGetMarginResponseSchema(Struct): + id: Union[str, int] + result: PublicGetMarginResultSchema + + +class PrivateGetSubaccountsResponseSchema(Struct): + id: Union[str, int] + result: PrivateGetSubaccountsResultSchema + + +class RFQResultPublicSchema(Struct): + cancel_reason: CancelReason + creation_timestamp: int + filled_pct: Decimal + last_update_timestamp: int + legs: List[LegUnpricedSchema] + partial_fill_step: Decimal + rfq_id: str + status: Status + subaccount_id: int + valid_until: int + filled_direction: Optional[Direction] = None + total_cost: Optional[Decimal] = None + + +class PrivateWithdrawResponseSchema(Struct): + id: Union[str, int] + result: PrivateWithdrawResultSchema + + +class PrivateUpdateNotificationsResponseSchema(Struct): + id: Union[str, int] + result: PrivateUpdateNotificationsResultSchema + + +class PrivateGetTradeHistoryResponseSchema(Struct): + id: Union[str, int] + result: PrivateGetTradeHistoryResultSchema + + +class PrivateOrderResponseSchema(Struct): + id: Union[str, int] + result: PrivateOrderResultSchema + + +class PublicGetInterestRateHistoryResultSchema(Struct): + interest_rates: List[InterestRateHistoryResponseSchema] + pagination: PaginationInfoSchema | None = None + + +class PublicGetOptionSettlementHistoryResponseSchema(Struct): + id: Union[str, int] + result: PublicGetOptionSettlementHistoryResultSchema + + +class PublicGetMakerProgramScoresResultSchema(Struct): + program: ProgramResponseSchema + scores: List[ScoreBreakdownSchema] + total_score: Decimal + total_volume: Decimal + + +class PrivateGetOrdersResponseSchema(Struct): + id: Union[str, int] + result: PrivateGetOrdersResultSchema + + +class PublicGetTickerResultSchema(Struct): + amount_step: Decimal + base_asset_address: str + base_asset_sub_id: str + base_currency: str + base_fee: Decimal + best_ask_amount: Decimal + best_ask_price: Decimal + best_bid_amount: Decimal + best_bid_price: Decimal + fifo_min_allocation: Decimal + five_percent_ask_depth: Decimal + five_percent_bid_depth: Decimal + index_price: Decimal + instrument_name: str + instrument_type: InstrumentType + is_active: bool + maker_fee_rate: Decimal + mark_price: Decimal + max_price: Decimal + maximum_amount: Decimal + min_price: Decimal + minimum_amount: Decimal + open_interest: Dict[str, List[OpenInterestStatsSchema]] + pro_rata_amount_step: Decimal + pro_rata_fraction: Decimal + quote_currency: str + scheduled_activation: int + scheduled_deactivation: int + stats: AggregateTradingStatsSchema + taker_fee_rate: Decimal + tick_size: Decimal + timestamp: int + erc20_details: Optional[ERC20PublicDetailsSchema] = None + option_details: Optional[OptionPublicDetailsSchema] = None + option_pricing: Optional[OptionPricingSchema] = None + perp_details: Optional[PerpPublicDetailsSchema] = None + mark_price_fee_rate_cap: Optional[Decimal] = None + + +class PrivateGetFundingHistoryResultSchema(Struct): + events: List[FundingPaymentSchema] + pagination: PaginationInfoSchema | None = None + + +class PublicGetSpotFeedHistoryResultSchema(Struct): + currency: str + spot_feed_history: List[SpotFeedHistoryResponseSchema] + + +class PrivateSetMmpConfigResponseSchema(Struct): + id: Union[str, int] + result: PrivateSetMmpConfigResultSchema + + +class PublicGetFundingRateHistoryResultSchema(Struct): + funding_rate_history: List[FundingRateSchema] + + +class PrivateGetNotificationsResultSchema(Struct): + notifications: List[NotificationResponseSchema] + pagination: PaginationInfoSchema | None = None + + +class PrivateCancelBatchQuotesResponseSchema(Struct): + id: Union[str, int] + result: PrivateCancelBatchQuotesResultSchema + + +class PrivateRfqGetBestQuoteResponseSchema(Struct): + id: Union[str, int] + result: PrivateRfqGetBestQuoteResultSchema + + +class PrivateDepositResponseSchema(Struct): + id: Union[str, int] + result: PrivateDepositResultSchema + + +class AuctionResultSchema(Struct): + auction_id: str + auction_type: AuctionType + bids: List[AuctionBidEventSchema] + fee: Decimal + start_timestamp: int + subaccount_id: int + tx_hash: str + end_timestamp: Optional[int] = None + + +class PrivateChangeSubaccountLabelResponseSchema(Struct): + id: Union[str, int] + result: PrivateChangeSubaccountLabelResultSchema + + +class PublicMarginWatchResultSchema(Struct): + collaterals: List[CollateralPublicResponseSchema] + currency: str + initial_margin: Decimal + maintenance_margin: Decimal + margin_type: MarginType + positions: List[PositionPublicResponseSchema] + subaccount_id: int + subaccount_value: Decimal + valuation_timestamp: int + + +class PublicGetTransactionResponseSchema(Struct): + id: Union[str, int] + result: PublicGetTransactionResultSchema + + +class PrivateGetErc20TransferHistoryResultSchema(Struct): + events: List[ERC20TransferSchema] + + +class PrivateReplaceResultSchema(Struct): + cancelled_order: OrderResponseSchema + create_order_error: Optional[RPCErrorFormatSchema] = None + order: Optional[OrderResponseSchema] = None + trades: Optional[List[TradeResponseSchema]] = None + + +class PublicGetTradeHistoryResultSchema(Struct): + trades: List[TradeSettledPublicResponseSchema] + pagination: PaginationInfoSchema | None = None + + +class PublicSendQuoteDebugResponseSchema(Struct): + id: Union[str, int] + result: PublicSendQuoteDebugResultSchema + + +class PrivateGetOrderHistoryResponseSchema(Struct): + id: Union[str, int] + result: PrivateGetOrderHistoryResultSchema + + +class PrivateCancelBatchRfqsResponseSchema(Struct): + id: Union[str, int] + result: PrivateCancelBatchRfqsResultSchema + + +class PrivateExecuteQuoteResponseSchema(Struct): + id: Union[str, int] + result: PrivateExecuteQuoteResultSchema + + +class PublicCreateSubaccountDebugResponseSchema(Struct): + id: Union[str, int] + result: PublicCreateSubaccountDebugResultSchema + + +class PrivateGetLiquidationHistoryResponseSchema(Struct): + id: Union[str, int] + result: List[AuctionResultSchema] + + +class ForwardFeedDataSchema(Struct): + confidence: Decimal + currency: str + deadline: int + expiry: int + fwd_diff: Decimal + signatures: OracleSignatureDataSchema + spot_aggregate_latest: Decimal + spot_aggregate_start: Decimal + timestamp: int + + +class VolFeedDataSchema(Struct): + confidence: Decimal + currency: str + deadline: int + expiry: int + signatures: OracleSignatureDataSchema + timestamp: int + vol_data: VolSVIParamDataSchema + + +class PublicGetReferralPerformanceResultSchema(Struct): + fee_share_percentage: Decimal + referral_code: str + rewards: Dict[str, Dict[str, Dict[str, ReferralPerformanceByInstrumentTypeSchema]]] + stdrv_balance: Decimal + total_fee_rewards: Decimal + total_notional_volume: Decimal + total_referred_fees: Decimal + + +class PrivateCreateSubaccountResponseSchema(Struct): + id: Union[str, int] + result: PrivateCreateSubaccountResultSchema + + +class PrivateCancelResponseSchema(Struct): + id: Union[str, int] + result: PrivateCancelResultSchema + + +class PublicGetSpotFeedHistoryCandlesResultSchema(Struct): + currency: str + spot_feed_history: List[SpotFeedHistoryCandlesResponseSchema] + + +class PrivateGetRfqsResultSchema(Struct): + rfqs: List[RFQResultSchema] + pagination: PaginationInfoSchema | None = None + + +class PrivateCancelByNonceResponseSchema(Struct): + id: Union[str, int] + result: PrivateCancelByNonceResultSchema + + +class PrivateGetSubaccountResponseSchema(Struct): + id: Union[str, int] + result: PrivateGetSubaccountResultSchema + + +class PrivateSendRfqResponseSchema(Struct): + id: Union[str, int] + result: PrivateSendRfqResultSchema + + +class PublicStatisticsResponseSchema(Struct): + id: Union[str, int] + result: PublicStatisticsResultSchema + + +class PublicGetAllInstrumentsResultSchema(Struct): + instruments: List[InstrumentPublicResponseSchema] + pagination: PaginationInfoSchema | None = None + + +class PrivateGetLiquidatorHistoryResponseSchema(Struct): + id: Union[str, int] + result: PrivateGetLiquidatorHistoryResultSchema + + +class PublicRegisterSessionKeyResponseSchema(Struct): + id: Union[str, int] + result: PublicRegisterSessionKeyResultSchema + + +class PublicGetVaultBalancesResponseSchema(Struct): + id: Union[str, int] + result: List[VaultBalanceResponseSchema] + + +class PrivateGetAccountResultSchema(Struct): + cancel_on_disconnect: bool + fee_info: AccountFeeInfoSchema + is_rfq_maker: bool + per_endpoint_tps: Dict[str, Any] + subaccount_ids: List[int] + wallet: str + websocket_matching_tps: int + websocket_non_matching_tps: int + websocket_option_tps: int + websocket_perp_tps: int + referral_code: Optional[str] = None + + +class PrivateCancelQuoteResponseSchema(Struct): + id: Union[str, int] + result: PrivateCancelQuoteResultSchema + + +class PrivateCancelByInstrumentResponseSchema(Struct): + id: Union[str, int] + result: PrivateCancelByInstrumentResultSchema + + +class PrivateSendQuoteResponseSchema(Struct): + id: Union[str, int] + result: PrivateSendQuoteResultSchema + + +class PrivateTransferPositionsParamsSchema(Struct): + maker_params: SignedQuoteParamsSchema + taker_params: SignedQuoteParamsSchema + wallet: str + + +class PrivateTransferPositionsResponseSchema(Struct): + id: Union[str, int] + result: PrivateTransferPositionsResultSchema + + +class PublicGetOptionSettlementPricesResponseSchema(Struct): + id: Union[str, int] + result: PublicGetOptionSettlementPricesResultSchema + + +class PrivateTransferPositionResponseSchema(Struct): + id: Union[str, int] + result: PrivateTransferPositionResultSchema + + +class PublicGetCurrencyResponseSchema(Struct): + id: Union[str, int] + result: PublicGetCurrencyResultSchema + + +class PrivateGetSubaccountValueHistoryResponseSchema(Struct): + id: Union[str, int] + result: PrivateGetSubaccountValueHistoryResultSchema + + +class PrivateOrderDebugResultSchema(Struct): + action_hash: str + encoded_data: str + encoded_data_hashed: str + raw_data: SignedTradeOrderSchema + typed_data_hash: str + + +class PrivateGetInterestHistoryResponseSchema(Struct): + id: Union[str, int] + result: PrivateGetInterestHistoryResultSchema + + +class PrivateGetDepositHistoryResponseSchema(Struct): + id: Union[str, int] + result: PrivateGetDepositHistoryResultSchema + + +class PrivateSessionKeysResponseSchema(Struct): + id: Union[str, int] + result: PrivateSessionKeysResultSchema + + +class PublicGetInstrumentsResponseSchema(Struct): + id: Union[str, int] + result: List[InstrumentPublicResponseSchema] + + +class PrivateGetAllPortfoliosResponseSchema(Struct): + id: Union[str, int] + result: List[PrivateGetSubaccountResultSchema] + + +class PrivatePollQuotesResponseSchema(Struct): + id: Union[str, int] + result: PrivatePollQuotesResultSchema + + +class PrivateGetWithdrawalHistoryResponseSchema(Struct): + id: Union[str, int] + result: PrivateGetWithdrawalHistoryResultSchema + + +class PublicGetLiveIncidentsResponseSchema(Struct): + id: Union[str, int] + result: PublicGetLiveIncidentsResultSchema + + +class PrivateGetOptionSettlementHistoryResponseSchema(Struct): + id: Union[str, int] + result: PrivateGetOptionSettlementHistoryResultSchema + + +class PublicGetVaultShareResponseSchema(Struct): + id: Union[str, int] + result: PublicGetVaultShareResultSchema + + +class PrivatePollRfqsResultSchema(Struct): + rfqs: List[RFQResultPublicSchema] + pagination: PaginationInfoSchema | None = None + + +class PublicGetInterestRateHistoryResponseSchema(Struct): + id: Union[str, int] + result: PublicGetInterestRateHistoryResultSchema + + +class PublicGetMakerProgramScoresResponseSchema(Struct): + id: Union[str, int] + result: PublicGetMakerProgramScoresResultSchema + + +class PublicGetTickerResponseSchema(Struct): + id: Union[str, int] + result: PublicGetTickerResultSchema + + +class PrivateGetFundingHistoryResponseSchema(Struct): + id: Union[str, int] + result: PrivateGetFundingHistoryResultSchema + + +class PublicGetSpotFeedHistoryResponseSchema(Struct): + id: Union[str, int] + result: PublicGetSpotFeedHistoryResultSchema + + +class PublicGetFundingRateHistoryResponseSchema(Struct): + id: Union[str, int] + result: PublicGetFundingRateHistoryResultSchema + + +class PrivateGetNotificationsResponseSchema(Struct): + id: Union[str, int] + result: PrivateGetNotificationsResultSchema + + +class PublicGetLiquidationHistoryResultSchema(Struct): + auctions: List[AuctionResultSchema] + pagination: PaginationInfoSchema | None = None + + +class PublicMarginWatchResponseSchema(Struct): + id: Union[str, int] + result: PublicMarginWatchResultSchema + + +class PrivateGetErc20TransferHistoryResponseSchema(Struct): + id: Union[str, int] + result: PrivateGetErc20TransferHistoryResultSchema + + +class PrivateReplaceResponseSchema(Struct): + id: Union[str, int] + result: PrivateReplaceResultSchema + + +class PublicGetTradeHistoryResponseSchema(Struct): + id: Union[str, int] + result: PublicGetTradeHistoryResultSchema + + +class PublicGetLatestSignedFeedsResultSchema(Struct): + fwd_data: Dict[str, Dict[str, ForwardFeedDataSchema]] + perp_data: Dict[str, Dict[str, PerpFeedDataSchema]] + rate_data: Dict[str, Dict[str, RateFeedDataSchema]] + spot_data: Dict[str, SpotFeedDataSchema] + vol_data: Dict[str, Dict[str, VolFeedDataSchema]] + + +class PublicGetReferralPerformanceResponseSchema(Struct): + id: Union[str, int] + result: PublicGetReferralPerformanceResultSchema + + +class PublicGetSpotFeedHistoryCandlesResponseSchema(Struct): + id: Union[str, int] + result: PublicGetSpotFeedHistoryCandlesResultSchema + + +class PrivateGetRfqsResponseSchema(Struct): + id: Union[str, int] + result: PrivateGetRfqsResultSchema + + +class PublicGetAllInstrumentsResponseSchema(Struct): + id: Union[str, int] + result: PublicGetAllInstrumentsResultSchema + + +class PrivateGetAccountResponseSchema(Struct): + id: Union[str, int] + result: PrivateGetAccountResultSchema + + +class PrivateOrderDebugResponseSchema(Struct): + id: Union[str, int] + result: PrivateOrderDebugResultSchema + + +class PrivatePollRfqsResponseSchema(Struct): + id: Union[str, int] + result: PrivatePollRfqsResultSchema + + +class PublicGetLiquidationHistoryResponseSchema(Struct): + id: Union[str, int] + result: PublicGetLiquidationHistoryResultSchema + + +class PublicGetLatestSignedFeedsResponseSchema(Struct): + id: Union[str, int] + result: PublicGetLatestSignedFeedsResultSchema diff --git a/derive_client/data_types/models.py b/derive_client/data_types/models.py index e083aef6..e743c33a 100644 --- a/derive_client/data_types/models.py +++ b/derive_client/data_types/models.py @@ -1,231 +1,281 @@ """Models used in the bridge module.""" -from typing import Any +from __future__ import annotations + +from dataclasses import dataclass +from decimal import Decimal +from typing import TYPE_CHECKING, Any, Literal, cast -from derive_action_signing.module_data import ModuleData -from derive_action_signing.utils import decimal_to_big_int -from eth_abi.abi import encode from eth_account.datastructures import SignedTransaction -from eth_utils import is_0x_prefixed, is_address, is_hex, to_checksum_address +from eth_typing import BlockNumber, HexStr +from eth_utils.address import is_address, to_checksum_address +from eth_utils.hexadecimal import is_0x_prefixed, is_hex from hexbytes import HexBytes from pydantic import ( BaseModel, ConfigDict, Field, - GetCoreSchemaHandler, - GetJsonSchemaHandler, HttpUrl, - PositiveFloat, RootModel, ) -from pydantic.dataclasses import dataclass -from pydantic_core import core_schema -from web3 import AsyncWeb3, Web3 -from web3.contract import AsyncContract -from web3.contract.async_contract import AsyncContractEvent -from web3.datastructures import AttributeDict +from web3 import AsyncWeb3 +from web3.contract.async_contract import AsyncContract, AsyncContractEvent +from web3.types import ChecksumAddress as ETHChecksumAddress +from web3.types import FilterParams, LogReceipt, TxReceipt +from web3.types import Wei as ETHWei from derive_client.exceptions import TxReceiptMissing -from .enums import ( - BridgeType, - ChainID, - Currency, - DeriveTxStatus, - GasPriority, - LiquidityRole, - MainnetCurrency, - MarginType, - OrderSide, - OrderStatus, - QuoteStatus, - SessionKeyScope, - TimeInForce, - TxStatus, -) +if TYPE_CHECKING: + from derive_client.constants import ChainID + from .enums import ( + BridgeType, + Currency, + GasPriority, + TxStatus, + ) -class PAttributeDict(AttributeDict): - @classmethod - def __get_pydantic_core_schema__(cls, _source, _handler: GetCoreSchemaHandler) -> core_schema.CoreSchema: - return core_schema.no_info_plain_validator_function(lambda v, **kwargs: cls._validate(v)) - - @classmethod - def __get_pydantic_json_schema__(cls, _schema, _handler: GetJsonSchemaHandler) -> dict: - return {"type": "object", "additionalProperties": True} - - @classmethod - def _validate(cls, v) -> AttributeDict: - if not isinstance(v, (dict, AttributeDict)): - raise TypeError(f"Expected AttributeDict, got {v!r}") - return AttributeDict(v) - - -class PHexBytes(HexBytes): - @classmethod - def __get_pydantic_core_schema__(cls, _source: Any, _handler: Any) -> core_schema.CoreSchema: - # Allow either HexBytes or bytes/hex strings to be parsed into HexBytes - return core_schema.no_info_before_validator_function( - cls._validate, - core_schema.union_schema( - [ - core_schema.is_instance_schema(HexBytes), - core_schema.bytes_schema(), - core_schema.str_schema(), - ] - ), - ) - @classmethod - def __get_pydantic_json_schema__(cls, _schema: core_schema.CoreSchema, _handler: Any) -> dict: - return {"type": "string", "format": "hex"} - - @classmethod - def _validate(cls, v: Any) -> HexBytes: - if isinstance(v, HexBytes): - return v - if isinstance(v, (bytes, bytearray)): - return HexBytes(v) - if isinstance(v, str): - return HexBytes(v) - raise TypeError(f"Expected HexBytes-compatible type, got {type(v).__name__}") - - -class PSignedTransaction(SignedTransaction): - @classmethod - def __get_pydantic_core_schema__(cls, _source: Any, _handler: Any) -> core_schema.CoreSchema: - # Accept existing SignedTransaction or a tuple/dict of its fields - return core_schema.no_info_plain_validator_function(cls._validate) - - @classmethod - def __get_pydantic_json_schema__(cls, _schema: core_schema.CoreSchema, _handler: Any) -> dict: - return { - "type": "object", - "properties": { - "raw_transaction": {"type": "string", "format": "hex"}, - "hash": {"type": "string", "format": "hex"}, - "r": {"type": "integer"}, - "s": {"type": "integer"}, - "v": {"type": "integer"}, - }, - } +class ChecksumAddress(str): + """ChecksumAddress with validation.""" - @classmethod - def _validate(cls, v: Any) -> SignedTransaction: - if isinstance(v, SignedTransaction): - return v - if isinstance(v, dict): - return SignedTransaction( - raw_transaction=PHexBytes(v["raw_transaction"]), - hash=PHexBytes(v["hash"]), - r=int(v["r"]), - s=int(v["s"]), - v=int(v["v"]), - ) - raise TypeError(f"Expected SignedTransaction or dict, got {type(v).__name__}") + def __new__(cls, v: str) -> ChecksumAddress: + if not is_address(v): + raise ValueError(f"Invalid Ethereum address: {v}") + return cast(ChecksumAddress, to_checksum_address(v)) -class Address(str): - @classmethod - def __get_pydantic_core_schema__(cls, _source, _handler: GetCoreSchemaHandler) -> core_schema.CoreSchema: - return core_schema.no_info_before_validator_function(cls._validate, core_schema.any_schema()) +class TxHash(str): + """Transaction hash with validation.""" - @classmethod - def __get_pydantic_json_schema__(cls, _schema, _handler: GetJsonSchemaHandler) -> dict: - return {"type": "string", "format": "ethereum-address"} + def __new__(cls, value: str | HexBytes) -> TxHash: + if isinstance(value, HexBytes): + value = value.hex() + if not isinstance(value, str): + raise TypeError(f"Expected string or HexBytes, got {type(value)}") + if not is_0x_prefixed(value) or not is_hex(value) or len(value) != 66: + raise ValueError(f"Invalid transaction hash: {value}") + return cast(TxHash, value) - @classmethod - def _validate(cls, v: str) -> str: - if not is_address(v): - raise ValueError(f"Invalid Ethereum address: {v}") - return to_checksum_address(v) +class Wei(int): + """Wei with validation.""" -class TxHash(str): - @classmethod - def __get_pydantic_core_schema__(cls, _source, _handler: GetCoreSchemaHandler): - return core_schema.no_info_before_validator_function(cls._validate, core_schema.str_schema()) + def __new__(cls, value: str | int) -> Wei: + if isinstance(value, str) and is_hex(value): + value = int(value, 16) + return cast(Wei, value) - @classmethod - def __get_pydantic_json_schema__(cls, _schema, _handler: GetJsonSchemaHandler): - return {"type": "string", "format": "ethereum-tx-hash"} - @classmethod - def _validate(cls, v: str | HexBytes) -> str: - if isinstance(v, HexBytes): - v = v.to_0x_hex() - if not isinstance(v, str): - raise TypeError("Expected a string or HexBytes for TxHash") - if not is_0x_prefixed(v) or not is_hex(v) or len(v) != 66: - raise ValueError(f"Invalid Ethereum transaction hash: {v}") - return v +class TypedFilterParams(BaseModel): + """Typed filter params for eth_getLogs that we actually use. + Unlike web3.types.FilterParams which has overly-broad unions, + this reflects our actual runtime behavior: + - We work with int block numbers internally + - We convert to hex strings right before RPC calls + - We use 'latest' as a special case for open-ended queries + """ -class Wei(int): - @classmethod - def __get_pydantic_core_schema__(cls, _source, _handler: GetCoreSchemaHandler) -> core_schema.CoreSchema: - return core_schema.no_info_before_validator_function(cls._validate, core_schema.int_schema()) + model_config = ConfigDict(frozen=True) - @classmethod - def __get_pydantic_json_schema__(cls, _schema, _handler: GetJsonSchemaHandler) -> dict: - return {"type": ["string", "integer"], "title": "Wei"} + address: ChecksumAddress | list[ChecksumAddress] + topics: tuple[HexBytes | None, ...] | None = None - @classmethod - def _validate(cls, v: str | int) -> int: - if isinstance(v, int): - return v - if isinstance(v, str) and is_hex(v): - return int(v, 16) - raise TypeError(f"Invalid type for Wei: {type(v)}") + # Block range - we use int internally, convert to hex for RPC + # 'latest' is used as sentinel for open-ended queries + fromBlock: int | Literal["latest"] + toBlock: int | Literal["latest"] + blockHash: HexBytes | None = None + def to_rpc_params(self) -> FilterParams: + """Convert to RPC-compatible filter params with hex block numbers.""" -@dataclass -class CreateSubAccountDetails: - amount: int - base_asset_address: str - sub_asset_address: str - - def to_eth_tx_params(self): - return ( - decimal_to_big_int(self.amount), - Web3.to_checksum_address(self.base_asset_address), - Web3.to_checksum_address(self.sub_asset_address), + address: ETHChecksumAddress | list[ETHChecksumAddress] + if isinstance(self.address, list): + address = [cast(ETHChecksumAddress, addr) for addr in self.address] + else: + address = cast(ETHChecksumAddress, self.address) + + from_block = cast(HexStr, hex(self.fromBlock)) if self.fromBlock != "latest" else self.fromBlock + to_block = cast(HexStr, hex(self.toBlock)) if self.toBlock != "latest" else self.toBlock + + params: FilterParams = { + "address": address, + "fromBlock": from_block, + "toBlock": to_block, + } + + if self.topics is not None: + params["topics"] = list(self.topics) + if self.blockHash is not None: + params["blockHash"] = self.blockHash + + return params + + +class TypedLogReceipt(BaseModel): + """Typed log entry from transaction receipt.""" + + address: ChecksumAddress + blockHash: HexBytes + blockNumber: int + data: HexBytes + logIndex: int + removed: bool + topics: list[HexBytes] + transactionHash: HexBytes + transactionIndex: int + + def to_w3(self) -> LogReceipt: + """Convert to web3.py LogReceipt dict.""" + + return LogReceipt( + address=cast(ETHChecksumAddress, self.address), + blockHash=self.blockHash, + blockNumber=cast(BlockNumber, self.blockNumber), + data=self.data, + logIndex=self.logIndex, + removed=self.removed, + topics=self.topics, + transactionHash=self.transactionHash, + transactionIndex=self.transactionIndex, ) -@dataclass -class CreateSubAccountData(ModuleData): - amount: int - asset_name: str - margin_type: str - create_account_details: CreateSubAccountDetails - - def to_abi_encoded(self): - return encode( - ['uint256', 'address', 'address'], - self.create_account_details.to_eth_tx_params(), +class TypedTxReceipt(BaseModel): + """Fully typed transaction receipt with attribute access. + + Based on web3.types.TxReceipt but actually usable with type checkers. + All fields from EIP-658 and common extensions included. + """ + + model_config = ConfigDict(populate_by_name=True) + + blockHash: HexBytes + blockNumber: int + contractAddress: ChecksumAddress | None + cumulativeGasUsed: int + effectiveGasPrice: int + from_: ChecksumAddress = Field(alias='from') + gasUsed: int + logs: list[TypedLogReceipt] + logsBloom: HexBytes + status: int # 0 or 1 per EIP-658 + to: ChecksumAddress + transactionHash: HexBytes + transactionIndex: int + type: int = Field(alias='type') # Transaction type (0=legacy, 1=EIP-2930, 2=EIP-1559) + + # Optional fields (depending on chain/tx type) + root: HexStr # Pre-EIP-658 state root + # blobGasPrice: int | None = None # EIP-4844 + # blobGasUsed: int | None = None # EIP-4844 + + def to_w3(self) -> TxReceipt: + """Convert to web3.py TxReceipt dict.""" + + return { + 'blockHash': self.blockHash, + 'blockNumber': cast(BlockNumber, self.blockNumber), + 'contractAddress': cast(ETHChecksumAddress, self.contractAddress) if self.contractAddress else None, + 'cumulativeGasUsed': self.cumulativeGasUsed, + 'effectiveGasPrice': cast(ETHWei, self.effectiveGasPrice), + 'from': cast(ETHChecksumAddress, self.from_), + 'gasUsed': self.gasUsed, + 'logs': [log.to_w3() for log in self.logs], + 'logsBloom': self.logsBloom, + 'status': self.status, + 'to': cast(ETHChecksumAddress, self.to), + 'transactionHash': self.transactionHash, + 'transactionIndex': self.transactionIndex, + 'type': self.type, + 'root': self.root, + } + + # return tx_receipt + + +class TypedSignedTransaction(BaseModel): + """Properly typed signed transaction. + + Immutable replacement for eth_account.datastructures.SignedTransaction. + """ + + model_config = ConfigDict(frozen=True) + + raw_transaction: HexBytes + hash: HexBytes + r: int + s: int + v: int + + def to_w3(self) -> SignedTransaction: + """Convert to eth_account SignedTransaction.""" + + return SignedTransaction( + raw_transaction=self.raw_transaction, + hash=self.hash, + r=self.r, + s=self.s, + v=self.v, ) - def to_json(self): - return {} + +class TypedTransaction(BaseModel): + """Fully typed transaction data retrieved from the blockchain. + + Based on web3.types.TxData but with proper attribute access. + This represents a transaction that has been retrieved from a node, + which may or may not be mined yet. + """ + + model_config = ConfigDict(populate_by_name=True) + + blockHash: HexBytes | None + blockNumber: int | None # None if pending + from_: ChecksumAddress = Field(alias='from') + gas: int + gasPrice: int | None = None # Legacy transactions + maxFeePerGas: int | None = None # EIP-1559 + maxPriorityFeePerGas: int | None = None # EIP-1559 + hash: HexBytes + input: HexBytes + nonce: int + to: ChecksumAddress | None # None for contract creation + transactionIndex: int | None # None if pending + value: int + type: int # 0=legacy, 1=EIP-2930, 2=EIP-1559 + chainId: int | None = None + v: int + r: HexBytes + s: HexBytes + + # EIP-2930 (optional) + accessList: list[dict[str, Any]] | None = None + + # EIP-4844 (optional) + maxFeePerBlobGas: int | None = None + blobVersionedHashes: list[HexBytes] | None = None class TokenData(BaseModel): isAppChain: bool - connectors: dict[ChainID, dict[str, str]] - LyraTSAShareHandlerDepositHook: Address | None = None - LyraTSADepositHook: Address | None = None + connectors: dict[ChainID, dict[str, ChecksumAddress]] + LyraTSAShareHandlerDepositHook: ChecksumAddress | None = None + LyraTSADepositHook: ChecksumAddress | None = None isNewBridge: bool class MintableTokenData(TokenData): - Controller: Address - MintableToken: Address + Controller: ChecksumAddress + MintableToken: ChecksumAddress class NonMintableTokenData(TokenData): - Vault: Address - NonMintableToken: Address + Vault: ChecksumAddress + NonMintableToken: ChecksumAddress class DeriveAddresses(BaseModel): @@ -233,21 +283,7 @@ class DeriveAddresses(BaseModel): chains: dict[ChainID, dict[Currency, MintableTokenData | NonMintableTokenData]] -class SessionKey(BaseModel): - public_session_key: Address - expiry_sec: int - ip_whitelist: list - label: str - scope: SessionKeyScope - - -class ManagerAddress(BaseModel): - address: Address - margin_type: MarginType - currency: MainnetCurrency | None - - -@dataclass(config=ConfigDict(arbitrary_types_allowed=True)) +@dataclass class BridgeContext: currency: Currency source_w3: AsyncWeb3 @@ -263,13 +299,12 @@ def bridge_type(self) -> BridgeType: return BridgeType.LAYERZERO if self.currency == Currency.DRV else BridgeType.SOCKET -@dataclass -class BridgeTxDetails: - contract: Address - method: str - kwargs: dict[str, Any] +class BridgeTxDetails(BaseModel): + contract: ChecksumAddress + fn_name: str + fn_kwargs: dict[str, Any] tx: dict[str, Any] - signed_tx: PSignedTransaction + signed_tx: TypedSignedTransaction @property def tx_hash(self) -> str: @@ -282,7 +317,7 @@ def nonce(self) -> int: return self.tx["nonce"] @property - def gas(self) -> int: + def gas(self) -> Wei: """Gas limit""" return self.tx["gas"] @@ -291,8 +326,7 @@ def max_fee_per_gas(self) -> Wei: return self.tx["maxFeePerGas"] -@dataclass -class PreparedBridgeTx: +class PreparedBridgeTx(BaseModel): amount: int value: int currency: Currency @@ -330,22 +364,21 @@ def nonce(self) -> int: return self.tx_details.nonce @property - def gas(self) -> int: + def gas(self) -> Wei: return self.tx_details.gas @property - def max_fee_per_gas(self) -> Wei: + def max_fee_per_gas(self) -> int: return self.tx_details.max_fee_per_gas @property - def max_total_fee(self) -> Wei: + def max_total_fee(self) -> int: return self.gas * self.max_fee_per_gas -@dataclass(config=ConfigDict(validate_assignment=True)) -class TxResult: +class TxResult(BaseModel): tx_hash: TxHash - tx_receipt: PAttributeDict | None = None + tx_receipt: TypedTxReceipt | None = None @property def status(self) -> TxStatus: @@ -354,8 +387,7 @@ def status(self) -> TxStatus: return TxStatus.PENDING -@dataclass(config=ConfigDict(validate_assignment=True)) -class BridgeTxResult: +class BridgeTxResult(BaseModel): prepared_tx: PreparedBridgeTx source_tx: TxResult target_from_block: int @@ -388,46 +420,19 @@ def bridge_type(self) -> BridgeType: def gas_used(self) -> int: if not self.source_tx.tx_receipt: raise TxReceiptMissing("Source tx receipt not available") - return self.source_tx.tx_receipt["gasUsed"] + return self.source_tx.tx_receipt.gasUsed @property - def effective_gas_price(self) -> Wei: + def effective_gas_price(self) -> int: if not self.source_tx.tx_receipt: raise TxReceiptMissing("Source tx receipt not available") - return self.source_tx.tx_receipt["effectiveGasPrice"] + return self.source_tx.tx_receipt.effectiveGasPrice @property - def total_fee(self) -> Wei: + def total_fee(self) -> int: return self.gas_used * self.effective_gas_price -class DepositResult(BaseModel): - status: DeriveTxStatus # should be "REQUESTED" - transaction_id: str - - -class WithdrawResult(BaseModel): - status: DeriveTxStatus # should be "REQUESTED" - transaction_id: str - - -class TransferPosition(BaseModel): - """Model for position transfer data.""" - - # Ref: https://docs.pydantic.dev/2.3/usage/types/number_types/#constrained-types - instrument_name: str - amount: PositiveFloat - limit_price: PositiveFloat - - -class DeriveTxResult(BaseModel): - data: dict # Data used to create transaction - status: DeriveTxStatus - error_log: dict - transaction_id: str - tx_hash: str | None = Field(alias="transaction_hash") - - class RPCEndpoints(BaseModel, frozen=True): ETH: list[HttpUrl] = Field(default_factory=list) OPTIMISM: list[HttpUrl] = Field(default_factory=list) @@ -469,104 +474,9 @@ def items(self): return self.root.items() -class Order(BaseModel): - amount: float - average_price: float - cancel_reason: str - creation_timestamp: int - direction: OrderSide - filled_amount: float - instrument_name: str - is_transfer: bool - label: str - last_update_timestamp: int - limit_price: float - max_fee: float - mmp: bool - nonce: int - order_fee: float - order_id: str - order_status: OrderStatus - order_type: str - quote_id: None - replaced_order_id: str | None - signature: str - signature_expiry_sec: int - signer: str - subaccount_id: int - time_in_force: TimeInForce - trigger_price: float | None - trigger_price_type: str | None - trigger_reject_message: str | None - trigger_type: str | None - - -class Trade(BaseModel): - direction: OrderSide - expected_rebate: float - index_price: float - instrument_name: str - is_transfer: bool - label: str - liquidity_role: LiquidityRole - mark_price: float - order_id: str - quote_id: None - realized_pnl: float - realized_pnl_excl_fees: float - subaccount_id: int - timestamp: int - trade_amount: float - trade_fee: float - trade_id: str - trade_price: float - transaction_id: str - tx_hash: str | None - tx_status: DeriveTxStatus - - -class PositionSpec(BaseModel): - amount: float # negative allowed to indicate direction - instrument_name: str - - -class PositionTransfer(BaseModel): - maker_order: Order - taker_order: Order - maker_trade: Trade - taker_trade: Trade - +@dataclass +class PositionTransfer: + """Position to transfer between subaccounts.""" -class Leg(BaseModel): - amount: float - direction: OrderSide # TODO: PositionSide instrument_name: str - price: float = 0.0 - - -class Quote(BaseModel): - cancel_reason: str - creation_timestamp: int - direction: OrderSide - fee: float - fill_pct: int - is_transfer: bool - label: str - last_update_timestamp: int - legs: list[Leg] - legs_hash: str - liquidity_role: LiquidityRole - max_fee: float - mmp: bool - nonce: int - quote_id: str - rfq_id: str - signature: str - signature_expiry_sec: int - signer: Address - status: QuoteStatus - - -class PositionsTransfer(BaseModel): - maker_quote: Quote - taker_quote: Quote + amount: Decimal # Can be negative (sign indicates long/short) diff --git a/derive_client/data_types/utils.py b/derive_client/data_types/utils.py new file mode 100644 index 00000000..ca2eee6e --- /dev/null +++ b/derive_client/data_types/utils.py @@ -0,0 +1,24 @@ +from decimal import Decimal + + +def D(value: int | float | str | Decimal) -> Decimal: + """ + Helper function to cast int, float, and str to Decimal. + + Args: + value: The value to convert to Decimal + + Returns: + Decimal representation of the value + + Examples: + >>> D(0.1) + Decimal('0.1') + >>> D("123.456") + Decimal('123.456') + >>> D(42) + Decimal('42') + """ + if isinstance(value, Decimal): + return value + return Decimal(str(value)) From 36eaf7030b792798be0f8a6f61873f330ae13538 Mon Sep 17 00:00:00 2001 From: zarathustra Date: Sun, 2 Nov 2025 16:46:11 +0100 Subject: [PATCH 11/22] feat: make generate-sync-bridge-client --- Makefile | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 7950407c..78b71896 100644 --- a/Makefile +++ b/Makefile @@ -68,8 +68,8 @@ release: .PHONY: generate-models generate-models: python scripts/generate-models.py - poetry run ruff check --fix derive_client/data/generated/models.py - poetry run ruff format derive_client/data/generated/models.py + poetry run ruff check --fix derive_client/data_types/generated_models.py + poetry run ruff format derive_client/data_types/generated_models.py .PHONY: generate-rest-api generate-rest-api: @@ -83,3 +83,9 @@ generate-rest-async-http: python scripts/generate-rest-async-http.py poetry run ruff check --fix tests/test_clients/test_rest/test_async_http poetry run ruff format tests/test_clients/test_rest/test_async_http + +.PHONY: generate-sync-bridge-client +generate-sync-bridge-client: + python scripts/generate-sync-bridge-client.py + poetry run ruff check --fix derive_client/_bridge/client.py + poetry run ruff format derive_client/_bridge/client.py From 6936bcc50d8853b7312ed0678c62ee20817671e2 Mon Sep 17 00:00:00 2001 From: zarathustra Date: Sun, 2 Nov 2025 16:48:11 +0100 Subject: [PATCH 12/22] chore: remove fees.py --- derive_client/utils/fees.py | 123 ------------------------------------ 1 file changed, 123 deletions(-) delete mode 100644 derive_client/utils/fees.py diff --git a/derive_client/utils/fees.py b/derive_client/utils/fees.py deleted file mode 100644 index 0f2973b2..00000000 --- a/derive_client/utils/fees.py +++ /dev/null @@ -1,123 +0,0 @@ -"""Module for calculating the fees on Derive.""" - -# https://docs.derive.xyz/reference/fees-1 - -from decimal import Decimal -from enum import Enum - -from derive_client.data_types import InstrumentType, Leg, OrderSide - -SECONDS_PER_YEAR = 60 * 60 * 24 * 365 - - -class LegGroup(Enum): - LONG_CALLS = "long_calls" - SHORT_CALLS = "short_calls" - LONG_PUTS = "long_puts" - SHORT_PUTS = "short_puts" - PERPS = "perps" - - -def _is_box_spread(legs: list[Leg], tickers: dict) -> bool: - """ - 1. must have 4 legs - 2. all options - 3. same expiry - 4. one long call and short put at one strike price, - and one short call and a long put at another strike price - """ - - if len(legs) != 4: - return False - - options_details = [tickers[leg.instrument_name].get("options_details") for leg in legs] - if not all(options_details): - return False - - expiries = set() - strikes = dict() - for leg, details in zip(legs, options_details): - expiries.add(details["expiry"]) - strike = details["strike"] - option_type = details["option_type"] - strikes.setdefault(strike, dict()).setdefault(option_type, set()).add(leg.direction) - - if len(set(expiries)) != 1: - return False - - if len(strikes) != 2: - return False - - # check we have both calls and puts at each price - if not (set(positions) == {"C", "P"} for positions in strikes.values()): - return False - - # calls must be opposite, puts must be opposite - strike1_positions, strike2_positions = strikes.values() - call1, put1 = strike1_positions["C"], strike1_positions["P"] - call2, put2 = strike2_positions["C"], strike2_positions["P"] - return call1 != call2 and put1 != put2 - - -def _classify_leg(leg: Leg, ticker: dict): - instrument_type = InstrumentType(ticker["instrument_type"]) - option_type = ticker.get("option_details", {}).get("option_type", {}) - - match instrument_type, leg.direction, option_type: - case InstrumentType.PERP, _, _: - return LegGroup.PERPS - case InstrumentType.OPTION, OrderSide.BUY, "C": - return LegGroup.LONG_CALLS - case InstrumentType.OPTION, OrderSide.SELL, "C": - return LegGroup.SHORT_CALLS - case InstrumentType.OPTION, OrderSide.BUY, "P": - return LegGroup.LONG_PUTS - case InstrumentType.OPTION, OrderSide.SELL, "P": - return LegGroup.SHORT_PUTS - case _: - raise NotImplementedError() - - -def rfq_max_fee(client, legs: list[Leg], is_taker: bool = True) -> float: - """ - Max fee ($ for the full trade). - Request will be rejected if the supplied max fee is below the estimated fee for this trade. - DeriveJSONRPCException: Derive RPC 11023: Max fee order param is too low - """ - - tickers = {} - for leg in legs: - instrument_name = leg.instrument_name - ticker = client.fetch_ticker(instrument_name=instrument_name) - tickers[instrument_name] = ticker - - if _is_box_spread(legs, tickers): - first_ticker = tickers[legs[0]["instrument_name"]] - timestamp = int(first_ticker["timestamp"]) - expiry = int(first_ticker["option_details"]["expiry"]) - - strike1, strike2 = {Decimal(t["option_details"]["strike"]) for t in tickers.values()} - notional = abs(strike1 - strike2) - years_to_expiry = (expiry - timestamp / 1000) / SECONDS_PER_YEAR # why not use Fraction? - yield_spread_fee = notional * Decimal("0.01") * Decimal(str(years_to_expiry)) - - total_fee = yield_spread_fee - if is_taker: - total_fee += max(t["base_fee"] for t in tickers.values()) - - [Decimal(leg["amount"]) for leg in legs] - return total_fee - - # Normal multi-leg handling - for leg in legs: - ticker = tickers[leg.instrument_name] - _classify_leg(leg, ticker) - - base_fee = float(ticker["base_fee"]) - float(ticker["maker_fee_rate"]) - taker_fee_rate = float(ticker["taker_fee_rate"]) - index_price = float(ticker["index_price"]) - float(ticker["mark_price"]) - str(base_fee + index_price * taker_fee_rate) - - return From 2938a58af1e94e98d4395744855b6413a98072ce Mon Sep 17 00:00:00 2001 From: zarathustra Date: Sun, 2 Nov 2025 16:55:26 +0100 Subject: [PATCH 13/22] refactor: separate constants into constituents --- derive_client/config/__init__.py | 24 ++++ derive_client/config/constants.py | 33 +++++ .../{constants.py => config/contracts.py} | 134 +----------------- derive_client/config/networks.py | 99 +++++++++++++ 4 files changed, 162 insertions(+), 128 deletions(-) create mode 100644 derive_client/config/__init__.py create mode 100644 derive_client/config/constants.py rename derive_client/{constants.py => config/contracts.py} (62%) create mode 100644 derive_client/config/networks.py diff --git a/derive_client/config/__init__.py b/derive_client/config/__init__.py new file mode 100644 index 00000000..07065eac --- /dev/null +++ b/derive_client/config/__init__.py @@ -0,0 +1,24 @@ +from .constants import ( + ABI_DATA_DIR, + ASSUMED_BRIDGE_GAS_LIMIT, + DATA_DIR, + DEFAULT_GAS_FUNDING_AMOUNT, + DEFAULT_REFERER, + DEFAULT_RPC_ENDPOINTS, + DEFAULT_SPOT_QUOTE_TOKEN, + GAS_FEE_BUFFER, + GAS_LIMIT_BUFFER, + INT32_MAX, + INT64_MAX, + MIN_PRIORITY_FEE, + MSG_GAS_LIMIT, + PAYLOAD_SIZE, + PKG_ROOT, + PUBLIC_HEADERS, + TARGET_SPEED, + TEST_PRIVATE_KEY, + UINT32_MAX, + UINT64_MAX, +) +from .contracts import * +from .networks import * diff --git a/derive_client/config/constants.py b/derive_client/config/constants.py new file mode 100644 index 00000000..c7ca3312 --- /dev/null +++ b/derive_client/config/constants.py @@ -0,0 +1,33 @@ +"""Pure constants without dependencies.""" + +from pathlib import Path +from typing import Final + +INT32_MAX: Final[int] = (1 << 31) - 1 +UINT32_MAX: Final[int] = (1 << 32) - 1 +INT64_MAX: Final[int] = (1 << 63) - 1 +UINT64_MAX: Final[int] = (1 << 64) - 1 + + +PKG_ROOT = Path(__file__).parent.parent +DATA_DIR = PKG_ROOT / "data" +ABI_DATA_DIR = DATA_DIR / "abi" + +PUBLIC_HEADERS = {"accept": "application/json", "content-type": "application/json"} + +TEST_PRIVATE_KEY = "0xc14f53ee466dd3fc5fa356897ab276acbef4f020486ec253a23b0d1c3f89d4f4" +DEFAULT_SPOT_QUOTE_TOKEN = "USDC" + +DEFAULT_REFERER = "0x9135BA0f495244dc0A5F029b25CDE95157Db89AD" + +GAS_FEE_BUFFER = 1.1 # buffer multiplier to pad maxFeePerGas +GAS_LIMIT_BUFFER = 1.1 # buffer multiplier to pad gas limit +MSG_GAS_LIMIT = 200_000 +ASSUMED_BRIDGE_GAS_LIMIT = 1_000_000 +MIN_PRIORITY_FEE = 10_000 +PAYLOAD_SIZE = 161 +TARGET_SPEED = "FAST" + +DEFAULT_GAS_FUNDING_AMOUNT = int(0.0001 * 1e18) # 0.0001 ETH + +DEFAULT_RPC_ENDPOINTS = DATA_DIR / "rpc_endpoints.yaml" diff --git a/derive_client/constants.py b/derive_client/config/contracts.py similarity index 62% rename from derive_client/constants.py rename to derive_client/config/contracts.py index aaa8a3a9..4ddfdd9c 100644 --- a/derive_client/constants.py +++ b/derive_client/config/contracts.py @@ -1,19 +1,10 @@ -""" -Constants for Derive (formerly Lyra). -""" - -from enum import Enum, IntEnum -from pathlib import Path -from typing import Final +"""Contract addresses and environment configurations.""" from pydantic import BaseModel -from derive_client.data_types import ChecksumAddress, Currency, Environment, UnderlyingCurrency +from derive_client.data_types import ChecksumAddress, Environment -INT32_MAX: Final[int] = (1 << 31) - 1 -UINT32_MAX: Final[int] = (1 << 32) - 1 -INT64_MAX: Final[int] = (1 << 63) - 1 -UINT64_MAX: Final[int] = (1 << 64) - 1 +from .constants import ABI_DATA_DIR class ContractAddresses(BaseModel, frozen=True): @@ -46,9 +37,9 @@ class EnvConfig(BaseModel, frozen=True): contracts: ContractAddresses -PKG_ROOT = Path(__file__).parent -DATA_DIR = PKG_ROOT / "data" -ABI_DATA_DIR = DATA_DIR / "abi" +# PKG_ROOT = Path(__file__).parent.parent +# DATA_DIR = PKG_ROOT / "data" +# ABI_DATA_DIR = DATA_DIR / "abi" PUBLIC_HEADERS = {"accept": "application/json", "content-type": "application/json"} @@ -107,119 +98,6 @@ class EnvConfig(BaseModel, frozen=True): } -class ChainID(IntEnum): - ETH = 1 - OPTIMISM = 10 - DERIVE = LYRA = 957 - BASE = 8453 - MODE = 34443 - ARBITRUM = 42161 - BLAST = 81457 - - @classmethod - def _missing_(cls, value): - try: - int_value = int(value) - return next(member for member in cls if member == int_value) - except (ValueError, TypeError, StopIteration): - return super()._missing_(value) - - -class LayerZeroChainIDv2(IntEnum): - # https://docs.layerzero.network/v2/deployments/deployed-contracts - ETH = 30101 - ARBITRUM = 30110 - OPTIMISM = 30111 - BASE = 30184 - DERIVE = 30311 - - -class SocketAddress(Enum): - ETH = ChecksumAddress("0x943ac2775928318653e91d350574436a1b9b16f9") - ARBITRUM = ChecksumAddress("0x37cc674582049b579571e2ffd890a4d99355f6ba") - OPTIMISM = ChecksumAddress("0x301bD265F0b3C16A58CbDb886Ad87842E3A1c0a4") - BASE = ChecksumAddress("0x12E6e58864cE4402cF2B4B8a8E9c75eAD7280156") - DERIVE = ChecksumAddress("0x565810cbfa3Cf1390963E5aFa2fB953795686339") - - -class DeriveTokenAddress(Enum): - # https://www.coingecko.com/en/coins/derive - - # impl: 0x4909ad99441ea5311b90a94650c394cea4a881b8 (Derive) - ETH = ChecksumAddress("0xb1d1eae60eea9525032a6dcb4c1ce336a1de71be") - - # impl: 0x1eda1f6e04ae37255067c064ae783349cf10bdc5 (DeriveL2) - OPTIMISM = ChecksumAddress("0x33800de7e817a70a694f31476313a7c572bba100") - - # impl: 0x01259207a40925b794c8ac320456f7f6c8fe2636 (DeriveL2) - BASE = ChecksumAddress("0x9d0e8f5b25384c7310cb8c6ae32c8fbeb645d083") - - # impl: 0x5d22b63d83a9be5e054df0e3882592ceffcef097 (DeriveL2) - ARBITRUM = ChecksumAddress("0x77b7787a09818502305c95d68a2571f090abb135") - - # impl: 0x340B51Cb46DBF63B55deD80a78a40aa75Dd4ceDF (DeriveL2) - DERIVE = ChecksumAddress("0x2EE0fd70756EDC663AcC9676658A1497C247693A") - - -DEFAULT_REFERER = "0x9135BA0f495244dc0A5F029b25CDE95157Db89AD" - -GAS_FEE_BUFFER = 1.1 # buffer multiplier to pad maxFeePerGas -GAS_LIMIT_BUFFER = 1.1 # buffer multiplier to pad gas limit -MSG_GAS_LIMIT = 200_000 -ASSUMED_BRIDGE_GAS_LIMIT = 1_000_000 -MIN_PRIORITY_FEE = 10_000 -PAYLOAD_SIZE = 161 -TARGET_SPEED = "FAST" - -DEFAULT_GAS_FUNDING_AMOUNT = int(0.0001 * 1e18) # 0.0001 ETH - -TOKEN_DECIMALS = { - UnderlyingCurrency.ETH: 18, - UnderlyingCurrency.BTC: 8, - UnderlyingCurrency.USDC: 6, - UnderlyingCurrency.LBTC: 8, - UnderlyingCurrency.WEETH: 18, - UnderlyingCurrency.OP: 18, - UnderlyingCurrency.DRV: 18, - UnderlyingCurrency.rswETH: 18, - UnderlyingCurrency.rsETH: 18, - UnderlyingCurrency.DAI: 18, - UnderlyingCurrency.USDT: 6, - UnderlyingCurrency.OLAS: 18, - UnderlyingCurrency.DRV: 18, -} - -CURRENCY_DECIMALS = { - Currency.ETH: 18, - Currency.weETH: 18, - Currency.rswETH: 18, - Currency.rsETH: 18, - Currency.USDe: 18, - Currency.deUSD: 18, - Currency.PYUSD: 6, - Currency.sUSDe: 18, - Currency.SolvBTC: 18, - Currency.SolvBTCBBN: 18, - Currency.LBTC: 8, - Currency.OP: 18, - Currency.DAI: 18, - Currency.sDAI: 18, - Currency.cbBTC: 8, - Currency.eBTC: 8, - Currency.AAVE: 18, - Currency.OLAS: 18, - Currency.DRV: 18, - Currency.WBTC: 8, - Currency.WETH: 18, - Currency.USDC: 6, - Currency.USDT: 6, - Currency.wstETH: 18, - Currency.USDCe: 6, - Currency.SNX: 18, -} - -DEFAULT_RPC_ENDPOINTS = DATA_DIR / "rpc_endpoints.yaml" - NEW_VAULT_ABI_PATH = ABI_DATA_DIR / "socket_superbridge_vault.json" OLD_VAULT_ABI_PATH = ABI_DATA_DIR / "socket_superbridge_vault_old.json" DEPOSIT_HELPER_ABI_PATH = ABI_DATA_DIR / "deposit_helper.json" diff --git a/derive_client/config/networks.py b/derive_client/config/networks.py new file mode 100644 index 00000000..3c8210d5 --- /dev/null +++ b/derive_client/config/networks.py @@ -0,0 +1,99 @@ +"""Network and chain configurations.""" + +from enum import Enum, IntEnum + +from derive_client.data_types import ChecksumAddress, Currency, UnderlyingCurrency + + +class LayerZeroChainIDv2(IntEnum): + # https://docs.layerzero.network/v2/deployments/deployed-contracts + ETH = 30101 + ARBITRUM = 30110 + OPTIMISM = 30111 + BASE = 30184 + DERIVE = 30311 + + +class SocketAddress(Enum): + ETH = ChecksumAddress("0x943ac2775928318653e91d350574436a1b9b16f9") + ARBITRUM = ChecksumAddress("0x37cc674582049b579571e2ffd890a4d99355f6ba") + OPTIMISM = ChecksumAddress("0x301bD265F0b3C16A58CbDb886Ad87842E3A1c0a4") + BASE = ChecksumAddress("0x12E6e58864cE4402cF2B4B8a8E9c75eAD7280156") + DERIVE = ChecksumAddress("0x565810cbfa3Cf1390963E5aFa2fB953795686339") + + +class DeriveTokenAddress(Enum): + # https://www.coingecko.com/en/coins/derive + + # impl: 0x4909ad99441ea5311b90a94650c394cea4a881b8 (Derive) + ETH = ChecksumAddress("0xb1d1eae60eea9525032a6dcb4c1ce336a1de71be") + + # impl: 0x1eda1f6e04ae37255067c064ae783349cf10bdc5 (DeriveL2) + OPTIMISM = ChecksumAddress("0x33800de7e817a70a694f31476313a7c572bba100") + + # impl: 0x01259207a40925b794c8ac320456f7f6c8fe2636 (DeriveL2) + BASE = ChecksumAddress("0x9d0e8f5b25384c7310cb8c6ae32c8fbeb645d083") + + # impl: 0x5d22b63d83a9be5e054df0e3882592ceffcef097 (DeriveL2) + ARBITRUM = ChecksumAddress("0x77b7787a09818502305c95d68a2571f090abb135") + + # impl: 0x340B51Cb46DBF63B55deD80a78a40aa75Dd4ceDF (DeriveL2) + DERIVE = ChecksumAddress("0x2EE0fd70756EDC663AcC9676658A1497C247693A") + + +DEFAULT_REFERER = "0x9135BA0f495244dc0A5F029b25CDE95157Db89AD" + +GAS_FEE_BUFFER = 1.1 # buffer multiplier to pad maxFeePerGas +GAS_LIMIT_BUFFER = 1.1 # buffer multiplier to pad gas limit +MSG_GAS_LIMIT = 200_000 +ASSUMED_BRIDGE_GAS_LIMIT = 1_000_000 +MIN_PRIORITY_FEE = 10_000 +PAYLOAD_SIZE = 161 +TARGET_SPEED = "FAST" + +DEFAULT_GAS_FUNDING_AMOUNT = int(0.0001 * 1e18) # 0.0001 ETH + +TOKEN_DECIMALS = { + UnderlyingCurrency.ETH: 18, + UnderlyingCurrency.BTC: 8, + UnderlyingCurrency.USDC: 6, + UnderlyingCurrency.LBTC: 8, + UnderlyingCurrency.WEETH: 18, + UnderlyingCurrency.OP: 18, + UnderlyingCurrency.DRV: 18, + UnderlyingCurrency.rswETH: 18, + UnderlyingCurrency.rsETH: 18, + UnderlyingCurrency.DAI: 18, + UnderlyingCurrency.USDT: 6, + UnderlyingCurrency.OLAS: 18, + UnderlyingCurrency.DRV: 18, +} + +CURRENCY_DECIMALS = { + Currency.ETH: 18, + Currency.weETH: 18, + Currency.rswETH: 18, + Currency.rsETH: 18, + Currency.USDe: 18, + Currency.deUSD: 18, + Currency.PYUSD: 6, + Currency.sUSDe: 18, + Currency.SolvBTC: 18, + Currency.SolvBTCBBN: 18, + Currency.LBTC: 8, + Currency.OP: 18, + Currency.DAI: 18, + Currency.sDAI: 18, + Currency.cbBTC: 8, + Currency.eBTC: 8, + Currency.AAVE: 18, + Currency.OLAS: 18, + Currency.DRV: 18, + Currency.WBTC: 8, + Currency.WETH: 18, + Currency.USDC: 6, + Currency.USDT: 6, + Currency.wstETH: 18, + Currency.USDCe: 6, + Currency.SNX: 18, +} From d2f8151f915e549f57954ee33009a47eccb5e54e Mon Sep 17 00:00:00 2001 From: zarathustra Date: Sun, 2 Nov 2025 17:03:14 +0100 Subject: [PATCH 14/22] fix: data types --- derive_client/data/generated/models.py | 2646 ------------------ derive_client/data/templates/api.py.jinja | 4 +- derive_client/data_types/__init__.py | 2 + derive_client/data_types/enums.py | 18 + derive_client/data_types/generated_models.py | 2 +- derive_client/data_types/models.py | 140 +- 6 files changed, 135 insertions(+), 2677 deletions(-) delete mode 100644 derive_client/data/generated/models.py diff --git a/derive_client/data/generated/models.py b/derive_client/data/generated/models.py deleted file mode 100644 index e46204fa..00000000 --- a/derive_client/data/generated/models.py +++ /dev/null @@ -1,2646 +0,0 @@ -# ruff: noqa: E741 -from __future__ import annotations - -from decimal import Decimal -from enum import Enum -from typing import Any, Dict, List, Optional, Union - -from msgspec import Struct - - -class PublicGetVaultStatisticsParamsSchema(Struct): - pass - - -class VaultStatisticsResponseSchema(Struct): - base_value: Decimal - block_number: int - block_timestamp: int - total_supply: Decimal - usd_tvl: Decimal - usd_value: Decimal - vault_name: str - subaccount_value_at_last_trade: Optional[Decimal] = None - underlying_value: Optional[Decimal] = None - - -class Direction(str, Enum): - buy = 'buy' - sell = 'sell' - - -class LegPricedSchema(Struct): - amount: Decimal - direction: Direction - instrument_name: str - price: Decimal - - -class CancelReason(str, Enum): - field_ = '' - user_request = 'user_request' - insufficient_margin = 'insufficient_margin' - signed_max_fee_too_low = 'signed_max_fee_too_low' - mmp_trigger = 'mmp_trigger' - cancel_on_disconnect = 'cancel_on_disconnect' - session_key_deregistered = 'session_key_deregistered' - subaccount_withdrawn = 'subaccount_withdrawn' - rfq_no_longer_open = 'rfq_no_longer_open' - compliance = 'compliance' - - -class LiquidityRole(str, Enum): - maker = 'maker' - taker = 'taker' - - -class Status(str, Enum): - open = 'open' - filled = 'filled' - cancelled = 'cancelled' - expired = 'expired' - - -class TxStatus(str, Enum): - requested = 'requested' - pending = 'pending' - settled = 'settled' - reverted = 'reverted' - ignored = 'ignored' - timed_out = 'timed_out' - - -class QuoteResultSchema(Struct): - cancel_reason: CancelReason - creation_timestamp: int - direction: Direction - fee: Decimal - fill_pct: Decimal - is_transfer: bool - label: str - last_update_timestamp: int - legs: List[LegPricedSchema] - legs_hash: str - liquidity_role: LiquidityRole - max_fee: Decimal - mmp: bool - nonce: int - quote_id: str - rfq_id: str - signature: str - signature_expiry_sec: int - signer: str - status: Status - subaccount_id: int - tx_hash: Optional[str] = None - tx_status: Optional[TxStatus] = None - - -class PrivateResetMmpParamsSchema(Struct): - subaccount_id: int - currency: Optional[str] = None - - -class Result(str, Enum): - ok = 'ok' - - -class PrivateResetMmpResponseSchema(Struct): - id: Union[str, int] - result: Result - - -class PublicGetOptionSettlementPricesParamsSchema(Struct): - currency: str - - -class ExpiryResponseSchema(Struct): - expiry_date: str - utc_expiry_sec: int - price: Optional[Decimal] = None - - -class TradeModuleParamsSchema(Struct): - amount: Decimal - direction: Direction - instrument_name: str - limit_price: Decimal - max_fee: Decimal - nonce: int - signature: str - signature_expiry_sec: int - signer: str - subaccount_id: int - - -class CancelReason1(str, Enum): - field_ = '' - user_request = 'user_request' - mmp_trigger = 'mmp_trigger' - insufficient_margin = 'insufficient_margin' - signed_max_fee_too_low = 'signed_max_fee_too_low' - cancel_on_disconnect = 'cancel_on_disconnect' - ioc_or_market_partial_fill = 'ioc_or_market_partial_fill' - session_key_deregistered = 'session_key_deregistered' - subaccount_withdrawn = 'subaccount_withdrawn' - compliance = 'compliance' - trigger_failed = 'trigger_failed' - validation_failed = 'validation_failed' - - -class OrderStatus(str, Enum): - open = 'open' - filled = 'filled' - cancelled = 'cancelled' - expired = 'expired' - untriggered = 'untriggered' - - -class OrderType(str, Enum): - limit = 'limit' - market = 'market' - - -class TimeInForce(str, Enum): - gtc = 'gtc' - post_only = 'post_only' - fok = 'fok' - ioc = 'ioc' - - -class TriggerPriceType(str, Enum): - mark = 'mark' - index = 'index' - - -class TriggerType(str, Enum): - stoploss = 'stoploss' - takeprofit = 'takeprofit' - - -class OrderResponseSchema(Struct): - amount: Decimal - average_price: Decimal - cancel_reason: CancelReason1 - creation_timestamp: int - direction: Direction - filled_amount: Decimal - instrument_name: str - is_transfer: bool - label: str - last_update_timestamp: int - limit_price: Decimal - max_fee: Decimal - mmp: bool - nonce: int - order_fee: Decimal - order_id: str - order_status: OrderStatus - order_type: OrderType - signature: str - signature_expiry_sec: int - signer: str - subaccount_id: int - time_in_force: TimeInForce - quote_id: Optional[str] = None - replaced_order_id: Optional[str] = None - trigger_price: Optional[Decimal] = None - trigger_price_type: Optional[TriggerPriceType] = None - trigger_reject_message: Optional[str] = None - trigger_type: Optional[TriggerType] = None - - -class TradeResponseSchema(Struct): - direction: Direction - expected_rebate: Decimal - index_price: Decimal - instrument_name: str - is_transfer: bool - label: str - liquidity_role: LiquidityRole - mark_price: Decimal - order_id: str - realized_pnl: Decimal - realized_pnl_excl_fees: Decimal - subaccount_id: int - timestamp: int - trade_amount: Decimal - trade_fee: Decimal - trade_id: str - trade_price: Decimal - transaction_id: str - tx_status: TxStatus - quote_id: Optional[str] = None - tx_hash: Optional[str] = None - - -class PublicDepositDebugParamsSchema(Struct): - amount: Decimal - asset_name: str - nonce: int - signature_expiry_sec: int - signer: str - subaccount_id: int - is_atomic_signing: bool = False - - -class PublicDepositDebugResultSchema(Struct): - action_hash: str - encoded_data: str - encoded_data_hashed: str - typed_data_hash: str - - -class PrivateGetOpenOrdersParamsSchema(Struct): - subaccount_id: int - - -class PrivateGetOpenOrdersResultSchema(Struct): - orders: List[OrderResponseSchema] - subaccount_id: int - - -class SignatureDetailsSchema(Struct): - nonce: int - signature: str - signature_expiry_sec: int - signer: str - - -class TransferDetailsSchema(Struct): - address: str - amount: Decimal - sub_id: int - - -class PrivateTransferErc20ResultSchema(Struct): - status: str - transaction_id: str - - -class Scope(str, Enum): - admin = 'admin' - account = 'account' - read_only = 'read_only' - - -class PrivateRegisterScopedSessionKeyParamsSchema(Struct): - expiry_sec: int - public_session_key: str - wallet: str - ip_whitelist: Optional[List[str]] = None - label: Optional[str] = None - scope: Scope = 'read_only' - signed_raw_tx: Optional[str] = None - - -class PrivateRegisterScopedSessionKeyResultSchema(Struct): - expiry_sec: int - public_session_key: str - scope: Scope - ip_whitelist: Optional[List[str]] = None - label: Optional[str] = None - transaction_id: Optional[str] = None - - -class PublicGetCurrencyParamsSchema(PublicGetOptionSettlementPricesParamsSchema): - pass - - -class InstrumentType(str, Enum): - erc20 = 'erc20' - option = 'option' - perp = 'perp' - - -class MarketType(str, Enum): - ALL = 'ALL' - SRM_BASE_ONLY = 'SRM_BASE_ONLY' - SRM_OPTION_ONLY = 'SRM_OPTION_ONLY' - SRM_PERP_ONLY = 'SRM_PERP_ONLY' - CASH = 'CASH' - - -class OpenInterestStatsSchema(Struct): - current_open_interest: Decimal - interest_cap: Decimal - manager_currency: Optional[str] = None - - -class MarginType(str, Enum): - PM = 'PM' - SM = 'SM' - PM2 = 'PM2' - - -class ManagerContractResponseSchema(Struct): - address: str - margin_type: MarginType - currency: Optional[str] = None - - -class PM2CollateralDiscountsSchema(Struct): - im_discount: Decimal - manager_currency: str - mm_discount: Decimal - - -class ProtocolAssetAddressesSchema(Struct): - option: Optional[str] = None - perp: Optional[str] = None - spot: Optional[str] = None - underlying_erc20: Optional[str] = None - - -class PrivateLiquidateParamsSchema(Struct): - cash_transfer: Decimal - last_seen_trade_id: int - liquidated_subaccount_id: int - nonce: int - percent_bid: Decimal - price_limit: Decimal - signature: str - signature_expiry_sec: int - signer: str - subaccount_id: int - - -class PrivateLiquidateResultSchema(Struct): - estimated_bid_price: Decimal - estimated_discount_pnl: Decimal - estimated_percent_bid: Decimal - transaction_id: str - - -class PrivateGetSubaccountValueHistoryParamsSchema(Struct): - end_timestamp: int - period: int - start_timestamp: int - subaccount_id: int - - -class SubAccountValueHistoryResponseSchema(Struct): - subaccount_value: Decimal - timestamp: int - - -class PublicGetMakerProgramsParamsSchema(PublicGetVaultStatisticsParamsSchema): - pass - - -class ProgramResponseSchema(Struct): - asset_types: List[str] - currencies: List[str] - end_timestamp: int - min_notional: Decimal - name: str - rewards: Dict[str, Decimal] - start_timestamp: int - - -class PrivateOrderDebugParamsSchema(Struct): - amount: Decimal - direction: Direction - instrument_name: str - limit_price: Decimal - max_fee: Decimal - nonce: int - signature: str - signature_expiry_sec: int - signer: str - subaccount_id: int - is_atomic_signing: Optional[bool] = False - label: str = '' - mmp: bool = False - order_type: OrderType = 'limit' - reduce_only: bool = False - referral_code: str = '' - reject_timestamp: int = 9223372036854776000 - time_in_force: TimeInForce = 'gtc' - trigger_price: Optional[Decimal] = None - trigger_price_type: Optional[TriggerPriceType] = None - trigger_type: Optional[TriggerType] = None - - -class TradeModuleDataSchema(Struct): - asset: str - desired_amount: Decimal - is_bid: bool - limit_price: Decimal - recipient_id: int - sub_id: int - trade_id: str - worst_fee: Decimal - - -class PrivateGetInterestHistoryParamsSchema(Struct): - subaccount_id: int - end_timestamp: int = 9223372036854776000 - start_timestamp: int = 0 - - -class InterestPaymentSchema(Struct): - interest: Decimal - timestamp: int - - -class PrivateGetDepositHistoryParamsSchema(PrivateGetInterestHistoryParamsSchema): - pass - - -class DepositSchema(Struct): - amount: Decimal - asset: str - timestamp: int - transaction_id: str - tx_hash: str - tx_status: TxStatus - error_log: Optional[Dict[str, Any]] = None - - -class PrivateGetMmpConfigParamsSchema(PrivateResetMmpParamsSchema): - pass - - -class MMPConfigResultSchema(Struct): - currency: str - is_frozen: bool - mmp_frozen_time: int - mmp_interval: int - mmp_unfreeze_time: int - subaccount_id: int - mmp_amount_limit: Decimal = '0' - mmp_delta_limit: Decimal = '0' - - -class PrivateSessionKeysParamsSchema(Struct): - wallet: str - - -class SessionKeyResponseSchema(Struct): - expiry_sec: int - ip_whitelist: List[str] - label: str - public_session_key: str - scope: str - - -class PublicGetInstrumentsParamsSchema(Struct): - currency: str - expired: bool - instrument_type: InstrumentType - - -class ERC20PublicDetailsSchema(Struct): - decimals: int - borrow_index: Decimal = '1' - supply_index: Decimal = '1' - underlying_erc20_address: str = '' - - -class OptionType(str, Enum): - C = 'C' - P = 'P' - - -class OptionPublicDetailsSchema(Struct): - expiry: int - index: str - option_type: OptionType - strike: Decimal - settlement_price: Optional[Decimal] = None - - -class PerpPublicDetailsSchema(Struct): - aggregate_funding: Decimal - funding_rate: Decimal - index: str - max_rate_per_hour: Decimal - min_rate_per_hour: Decimal - static_interest_rate: Decimal - - -class PrivateGetAllPortfoliosParamsSchema(PrivateSessionKeysParamsSchema): - pass - - -class CollateralResponseSchema(Struct): - amount: Decimal - amount_step: Decimal - asset_name: str - asset_type: InstrumentType - average_price: Decimal - average_price_excl_fees: Decimal - creation_timestamp: int - cumulative_interest: Decimal - currency: str - delta: Decimal - delta_currency: str - initial_margin: Decimal - maintenance_margin: Decimal - mark_price: Decimal - mark_value: Decimal - open_orders_margin: Decimal - pending_interest: Decimal - realized_pnl: Decimal - realized_pnl_excl_fees: Decimal - total_fees: Decimal - unrealized_pnl: Decimal - unrealized_pnl_excl_fees: Decimal - - -class PositionResponseSchema(Struct): - amount: Decimal - amount_step: Decimal - average_price: Decimal - average_price_excl_fees: Decimal - creation_timestamp: int - cumulative_funding: Decimal - delta: Decimal - gamma: Decimal - index_price: Decimal - initial_margin: Decimal - instrument_name: str - instrument_type: InstrumentType - maintenance_margin: Decimal - mark_price: Decimal - mark_value: Decimal - net_settlements: Decimal - open_orders_margin: Decimal - pending_funding: Decimal - realized_pnl: Decimal - realized_pnl_excl_fees: Decimal - theta: Decimal - total_fees: Decimal - unrealized_pnl: Decimal - unrealized_pnl_excl_fees: Decimal - vega: Decimal - leverage: Optional[Decimal] = None - liquidation_price: Optional[Decimal] = None - - -class PublicGetInstrumentParamsSchema(Struct): - instrument_name: str - - -class PublicGetInstrumentResultSchema(Struct): - amount_step: Decimal - base_asset_address: str - base_asset_sub_id: str - base_currency: str - base_fee: Decimal - fifo_min_allocation: Decimal - instrument_name: str - instrument_type: InstrumentType - is_active: bool - maker_fee_rate: Decimal - maximum_amount: Decimal - minimum_amount: Decimal - pro_rata_amount_step: Decimal - pro_rata_fraction: Decimal - quote_currency: str - scheduled_activation: int - scheduled_deactivation: int - taker_fee_rate: Decimal - tick_size: Decimal - erc20_details: Optional[ERC20PublicDetailsSchema] = None - option_details: Optional[OptionPublicDetailsSchema] = None - perp_details: Optional[PerpPublicDetailsSchema] = None - mark_price_fee_rate_cap: Optional[Decimal] = None - - -class PublicExecuteQuoteDebugParamsSchema(Struct): - direction: Direction - legs: List[LegPricedSchema] - max_fee: Decimal - nonce: int - quote_id: str - rfq_id: str - signature: str - signature_expiry_sec: int - signer: str - subaccount_id: int - label: str = '' - - -class PublicExecuteQuoteDebugResultSchema(Struct): - action_hash: str - encoded_data: str - encoded_data_hashed: str - encoded_legs: str - legs_hash: str - typed_data_hash: str - - -class PrivateGetCollateralsParamsSchema(PrivateGetOpenOrdersParamsSchema): - pass - - -class PrivateGetCollateralsResultSchema(Struct): - collaterals: List[CollateralResponseSchema] - subaccount_id: int - - -class PrivatePollQuotesParamsSchema(Struct): - subaccount_id: int - from_timestamp: int = 0 - page: int = 1 - page_size: int = 100 - quote_id: Optional[str] = None - rfq_id: Optional[str] = None - status: Optional[Status] = None - to_timestamp: int = 18446744073709552000 - - -class PaginationInfoSchema(Struct): - count: int - num_pages: int - - -class QuoteResultPublicSchema(Struct): - cancel_reason: CancelReason - creation_timestamp: int - direction: Direction - fill_pct: Decimal - last_update_timestamp: int - legs: List[LegPricedSchema] - legs_hash: str - liquidity_role: LiquidityRole - quote_id: str - rfq_id: str - status: Status - subaccount_id: int - wallet: str - tx_hash: Optional[str] = None - tx_status: Optional[TxStatus] = None - - -class SimulatedCollateralSchema(Struct): - amount: Decimal - asset_name: str - - -class SimulatedPositionSchema(Struct): - amount: Decimal - instrument_name: str - entry_price: Optional[Decimal] = None - - -class PrivateGetMarginResultSchema(Struct): - is_valid_trade: bool - post_initial_margin: Decimal - post_maintenance_margin: Decimal - pre_initial_margin: Decimal - pre_maintenance_margin: Decimal - subaccount_id: int - - -class PublicBuildRegisterSessionKeyTxParamsSchema(Struct): - expiry_sec: int - public_session_key: str - wallet: str - gas: Optional[int] = None - nonce: Optional[int] = None - - -class PublicBuildRegisterSessionKeyTxResultSchema(Struct): - tx_params: Dict[str, Any] - - -class PrivateCancelTriggerOrderParamsSchema(Struct): - order_id: str - subaccount_id: int - - -class PrivateCancelTriggerOrderResultSchema(OrderResponseSchema): - pass - - -class PrivateGetOrderParamsSchema(PrivateCancelTriggerOrderParamsSchema): - pass - - -class PrivateGetOrderResultSchema(OrderResponseSchema): - pass - - -class PrivateGetWithdrawalHistoryParamsSchema(PrivateGetInterestHistoryParamsSchema): - pass - - -class WithdrawalSchema(Struct): - amount: Decimal - asset: str - timestamp: int - tx_hash: str - tx_status: TxStatus - error_log: Optional[Dict[str, Any]] = None - - -class PublicGetLiveIncidentsParamsSchema(PublicGetVaultStatisticsParamsSchema): - pass - - -class MonitorType(str, Enum): - manual = 'manual' - auto = 'auto' - - -class Severity(str, Enum): - low = 'low' - medium = 'medium' - high = 'high' - - -class IncidentResponseSchema(Struct): - creation_timestamp_sec: int - label: str - message: str - monitor_type: MonitorType - severity: Severity - - -class PrivateGetQuotesParamsSchema(PrivatePollQuotesParamsSchema): - pass - - -class PrivateGetQuotesResultSchema(Struct): - quotes: List[QuoteResultSchema] - pagination: PaginationInfoSchema | None = None - - -class PrivateGetPositionsParamsSchema(PrivateGetOpenOrdersParamsSchema): - pass - - -class PrivateGetPositionsResultSchema(Struct): - positions: List[PositionResponseSchema] - subaccount_id: int - - -class PrivateGetOptionSettlementHistoryParamsSchema(PrivateGetOpenOrdersParamsSchema): - pass - - -class OptionSettlementResponseSchema(Struct): - amount: Decimal - expiry: int - instrument_name: str - option_settlement_pnl: Decimal - option_settlement_pnl_excl_fees: Decimal - settlement_price: Decimal - subaccount_id: int - - -class PublicDeregisterSessionKeyParamsSchema(Struct): - public_session_key: str - signed_raw_tx: str - wallet: str - - -class PublicDeregisterSessionKeyResultSchema(Struct): - public_session_key: str - transaction_id: str - - -class PublicGetVaultShareParamsSchema(Struct): - from_timestamp_sec: int - to_timestamp_sec: int - vault_name: str - page: int = 1 - page_size: int = 100 - - -class VaultShareResponseSchema(Struct): - base_value: Decimal - block_number: int - block_timestamp: int - usd_value: Decimal - underlying_value: Optional[Decimal] = None - - -class PrivateExpiredAndCancelledHistoryParamsSchema(Struct): - end_timestamp: int - expiry: int - start_timestamp: int - subaccount_id: int - wallet: str - - -class PrivateExpiredAndCancelledHistoryResultSchema(Struct): - presigned_urls: List[str] - - -class PrivateEditSessionKeyParamsSchema(Struct): - public_session_key: str - wallet: str - disable: bool = False - ip_whitelist: Optional[List[str]] = None - label: Optional[str] = None - - -class PrivateEditSessionKeyResultSchema(SessionKeyResponseSchema): - pass - - -class PublicGetAllCurrenciesParamsSchema(PublicGetVaultStatisticsParamsSchema): - pass - - -class CurrencyDetailedResponseSchema(Struct): - asset_cap_and_supply_per_manager: Dict[str, Dict[str, List[OpenInterestStatsSchema]]] - borrow_apy: Decimal - currency: str - instrument_types: List[InstrumentType] - managers: List[ManagerContractResponseSchema] - market_type: MarketType - pm2_collateral_discounts: List[PM2CollateralDiscountsSchema] - protocol_asset_addresses: ProtocolAssetAddressesSchema - spot_price: Decimal - srm_im_discount: Decimal - srm_mm_discount: Decimal - supply_apy: Decimal - total_borrow: Decimal - total_supply: Decimal - spot_price_24h: Optional[Decimal] = None - - -class PrivateCancelByLabelParamsSchema(Struct): - label: str - subaccount_id: int - instrument_name: Optional[str] = None - - -class PrivateCancelByLabelResultSchema(Struct): - cancelled_orders: int - - -class PublicWithdrawDebugParamsSchema(PublicDepositDebugParamsSchema): - pass - - -class PublicWithdrawDebugResultSchema(PublicDepositDebugResultSchema): - pass - - -class PublicGetMarginParamsSchema(Struct): - margin_type: MarginType - simulated_collaterals: List[SimulatedCollateralSchema] - simulated_positions: List[SimulatedPositionSchema] - market: Optional[str] = None - simulated_collateral_changes: Optional[List[SimulatedCollateralSchema]] = None - simulated_position_changes: Optional[List[SimulatedPositionSchema]] = None - - -class PublicGetMarginResultSchema(PrivateGetMarginResultSchema): - pass - - -class PrivateGetSubaccountsParamsSchema(PrivateSessionKeysParamsSchema): - pass - - -class PrivateGetSubaccountsResultSchema(Struct): - subaccount_ids: List[int] - wallet: str - - -class PrivatePollRfqsParamsSchema(Struct): - subaccount_id: int - from_timestamp: int = 0 - page: int = 1 - page_size: int = 100 - rfq_id: Optional[str] = None - rfq_subaccount_id: Optional[int] = None - status: Optional[Status] = None - to_timestamp: int = 18446744073709552000 - - -class LegUnpricedSchema(Struct): - amount: Decimal - direction: Direction - instrument_name: str - - -class PrivateWithdrawParamsSchema(Struct): - amount: Decimal - asset_name: str - nonce: int - signature: str - signature_expiry_sec: int - signer: str - subaccount_id: int - is_atomic_signing: bool = False - - -class PrivateWithdrawResultSchema(PrivateTransferErc20ResultSchema): - pass - - -class Status6(str, Enum): - unseen = 'unseen' - seen = 'seen' - hidden = 'hidden' - - -class PrivateUpdateNotificationsParamsSchema(Struct): - notification_ids: List[int] - subaccount_id: int - status: Status6 = 'seen' - - -class PrivateUpdateNotificationsResultSchema(Struct): - updated_count: int - - -class PrivateSetCancelOnDisconnectParamsSchema(Struct): - enabled: bool - wallet: str - - -class PrivateSetCancelOnDisconnectResponseSchema(PrivateResetMmpResponseSchema): - pass - - -class PrivateGetTradeHistoryParamsSchema(Struct): - from_timestamp: int = 0 - instrument_name: Optional[str] = None - order_id: Optional[str] = None - page: int = 1 - page_size: int = 100 - quote_id: Optional[str] = None - subaccount_id: Optional[int] = None - to_timestamp: int = 18446744073709552000 - wallet: Optional[str] = None - - -class PrivateGetTradeHistoryResultSchema(Struct): - subaccount_id: int - trades: List[TradeResponseSchema] - pagination: PaginationInfoSchema | None = None - - -class PrivateOrderParamsSchema(PrivateOrderDebugParamsSchema): - referral_code: str = '0x9135BA0f495244dc0A5F029b25CDE95157Db89AD' - pass - - -class PrivateOrderResultSchema(Struct): - order: OrderResponseSchema - trades: List[TradeResponseSchema] - - -class PublicGetInterestRateHistoryParamsSchema(Struct): - from_timestamp_sec: int - to_timestamp_sec: int - page: int = 1 - page_size: int = 100 - - -class InterestRateHistoryResponseSchema(Struct): - block: int - borrow_apy: Decimal - supply_apy: Decimal - timestamp_sec: int - total_borrow: Decimal - total_supply: Decimal - - -class PublicGetOptionSettlementHistoryParamsSchema(Struct): - page: int = 1 - page_size: int = 100 - subaccount_id: Optional[int] = None - - -class PublicGetOptionSettlementHistoryResultSchema(Struct): - settlements: List[OptionSettlementResponseSchema] - pagination: PaginationInfoSchema | None = None - - -class PublicGetMakerProgramScoresParamsSchema(Struct): - epoch_start_timestamp: int - program_name: str - - -class ScoreBreakdownSchema(Struct): - coverage_score: Decimal - holder_boost: Decimal - quality_score: Decimal - total_score: Decimal - volume: Decimal - volume_multiplier: Decimal - wallet: str - - -class PrivateGetOrdersParamsSchema(Struct): - subaccount_id: int - instrument_name: Optional[str] = None - label: Optional[str] = None - page: int = 1 - page_size: int = 100 - status: Optional[OrderStatus] = None - - -class PrivateGetOrdersResultSchema(Struct): - orders: List[OrderResponseSchema] - subaccount_id: int - pagination: PaginationInfoSchema | None = None - - -class PublicGetTickerParamsSchema(PublicGetInstrumentParamsSchema): - pass - - -class OptionPricingSchema(Struct): - ask_iv: Decimal - bid_iv: Decimal - delta: Decimal - discount_factor: Decimal - forward_price: Decimal - gamma: Decimal - iv: Decimal - mark_price: Decimal - rho: Decimal - theta: Decimal - vega: Decimal - - -class AggregateTradingStatsSchema(Struct): - contract_volume: Decimal - high: Decimal - low: Decimal - num_trades: Decimal - open_interest: Decimal - percent_change: Decimal - usd_change: Decimal - - -class PublicLoginParamsSchema(Struct): - signature: str - timestamp: str - wallet: str - - -class PublicLoginResponseSchema(Struct): - id: Union[str, int] - result: List[int] - - -class PrivateGetFundingHistoryParamsSchema(Struct): - subaccount_id: int - end_timestamp: int = 9223372036854776000 - instrument_name: Optional[str] = None - page: int = 1 - page_size: int = 100 - start_timestamp: int = 0 - - -class FundingPaymentSchema(Struct): - funding: Decimal - instrument_name: str - pnl: Decimal - timestamp: int - - -class PublicGetSpotFeedHistoryParamsSchema(Struct): - currency: str - end_timestamp: int - period: int - start_timestamp: int - - -class SpotFeedHistoryResponseSchema(Struct): - price: Decimal - timestamp: int - timestamp_bucket: int - - -class PrivateSetMmpConfigParamsSchema(Struct): - currency: str - mmp_frozen_time: int - mmp_interval: int - subaccount_id: int - mmp_amount_limit: Decimal = '0' - mmp_delta_limit: Decimal = '0' - - -class PrivateSetMmpConfigResultSchema(PrivateSetMmpConfigParamsSchema): - pass - - -class Period(str, Enum): - field_900 = 900 - field_3600 = 3600 - field_14400 = 14400 - field_28800 = 28800 - field_86400 = 86400 - - -class PublicGetFundingRateHistoryParamsSchema(Struct): - instrument_name: str - end_timestamp: int = 9223372036854776000 - period: Period = 3600 - start_timestamp: int = 0 - - -class FundingRateSchema(Struct): - funding_rate: Decimal - timestamp: int - - -class TypeEnum(str, Enum): - deposit = 'deposit' - withdraw = 'withdraw' - transfer = 'transfer' - trade = 'trade' - settlement = 'settlement' - liquidation = 'liquidation' - custom = 'custom' - - -class PrivateGetNotificationsParamsSchema(Struct): - page: Optional[int] = 1 - page_size: Optional[int] = 50 - status: Optional[Status6] = None - subaccount_id: Optional[int] = None - type: Optional[List[TypeEnum]] = None - wallet: Optional[str] = None - - -class NotificationResponseSchema(Struct): - event: str - event_details: Dict[str, Any] - id: int - status: str - subaccount_id: int - timestamp: int - transaction_id: Optional[int] = None - tx_hash: Optional[str] = None - - -class PrivateCancelBatchQuotesParamsSchema(Struct): - subaccount_id: int - label: Optional[str] = None - nonce: Optional[int] = None - quote_id: Optional[str] = None - rfq_id: Optional[str] = None - - -class PrivateCancelBatchQuotesResultSchema(Struct): - cancelled_ids: List[str] - - -class PrivateRfqGetBestQuoteParamsSchema(Struct): - legs: List[LegUnpricedSchema] - subaccount_id: int - counterparties: Optional[List[str]] = None - direction: Direction = 'buy' - label: str = '' - max_total_cost: Optional[Decimal] = None - min_total_cost: Optional[Decimal] = None - partial_fill_step: Decimal = '1' - rfq_id: Optional[str] = None - - -class InvalidReason(str, Enum): - Account_is_currently_under_maintenance_margin_requirements__trading_is_frozen_ = ( - 'Account is currently under maintenance margin requirements, trading is frozen.' - ) - This_order_would_cause_account_to_fall_under_maintenance_margin_requirements_ = ( - 'This order would cause account to fall under maintenance margin requirements.' - ) - Insufficient_buying_power__only_a_single_risk_reducing_open_order_is_allowed_ = ( - 'Insufficient buying power, only a single risk-reducing open order is allowed.' - ) - Insufficient_buying_power__consider_reducing_order_size_ = ( - 'Insufficient buying power, consider reducing order size.' - ) - Insufficient_buying_power__consider_reducing_order_size_or_canceling_other_orders_ = ( - 'Insufficient buying power, consider reducing order size or canceling other orders.' - ) - Consider_canceling_other_limit_orders_or_using_IOC__FOK__or_market_orders__This_order_is_risk_reducing__but_if_filled_with_other_open_orders__buying_power_might_be_insufficient_ = 'Consider canceling other limit orders or using IOC, FOK, or market orders. This order is risk-reducing, but if filled with other open orders, buying power might be insufficient.' - Insufficient_buying_power_ = 'Insufficient buying power.' - - -class PrivateRfqGetBestQuoteResultSchema(Struct): - direction: Direction - estimated_fee: Decimal - estimated_realized_pnl: Decimal - estimated_realized_pnl_excl_fees: Decimal - estimated_total_cost: Decimal - filled_pct: Decimal - is_valid: bool - post_initial_margin: Decimal - pre_initial_margin: Decimal - suggested_max_fee: Decimal - best_quote: Optional[QuoteResultPublicSchema] = None - down_liquidation_price: Optional[Decimal] = None - invalid_reason: Optional[InvalidReason] = None - post_liquidation_price: Optional[Decimal] = None - up_liquidation_price: Optional[Decimal] = None - - -class PrivateDepositParamsSchema(PrivateWithdrawParamsSchema): - pass - - -class PrivateDepositResultSchema(PrivateTransferErc20ResultSchema): - pass - - -class PublicGetLiquidationHistoryParamsSchema(Struct): - end_timestamp: int = 9223372036854776000 - page: int = 1 - page_size: int = 100 - start_timestamp: int = 0 - subaccount_id: Optional[int] = None - - -class AuctionType(str, Enum): - solvent = 'solvent' - insolvent = 'insolvent' - - -class AuctionBidEventSchema(Struct): - amounts_liquidated: Dict[str, Decimal] - cash_received: Decimal - discount_pnl: Decimal - percent_liquidated: Decimal - positions_realized_pnl: Dict[str, Decimal] - positions_realized_pnl_excl_fees: Dict[str, Decimal] - realized_pnl: Decimal - realized_pnl_excl_fees: Decimal - timestamp: int - tx_hash: str - - -class PrivateChangeSubaccountLabelParamsSchema(Struct): - label: str - subaccount_id: int - - -class PrivateChangeSubaccountLabelResultSchema(PrivateChangeSubaccountLabelParamsSchema): - pass - - -class PublicMarginWatchParamsSchema(Struct): - subaccount_id: int - force_onchain: bool = False - - -class CollateralPublicResponseSchema(Struct): - amount: Decimal - asset_name: str - asset_type: InstrumentType - initial_margin: Decimal - maintenance_margin: Decimal - mark_price: Decimal - mark_value: Decimal - - -class PositionPublicResponseSchema(Struct): - amount: Decimal - delta: Decimal - gamma: Decimal - index_price: Decimal - initial_margin: Decimal - instrument_name: str - instrument_type: InstrumentType - maintenance_margin: Decimal - mark_price: Decimal - mark_value: Decimal - theta: Decimal - vega: Decimal - liquidation_price: Optional[Decimal] = None - - -class PublicGetTransactionParamsSchema(Struct): - transaction_id: str - - -class TransactionDataInner(Struct): - asset: str - amount: str - decimals: int - - -class TransactionData(Struct): - data: TransactionDataInner - nonce: int - owner: str - expiry: int - module: str - signer: str - asset_id: str - signature: str - asset_name: str - subaccount_id: int - is_atomic_signing: bool - - -class TransactionErrorLog(Struct): - error: str - - -class PublicGetTransactionResultSchema(Struct): - data: TransactionData - status: TxStatus - error_log: Optional[TransactionErrorLog] = None - transaction_hash: Optional[str] = None - - -class PrivateGetErc20TransferHistoryParamsSchema(PrivateGetInterestHistoryParamsSchema): - pass - - -class ERC20TransferSchema(Struct): - amount: Decimal - asset: str - counterparty_subaccount_id: int - is_outgoing: bool - timestamp: int - tx_hash: str - - -class PrivateReplaceParamsSchema(Struct): - amount: Decimal - direction: Direction - instrument_name: str - limit_price: Decimal - max_fee: Decimal - nonce: int - signature: str - signature_expiry_sec: int - signer: str - subaccount_id: int - expected_filled_amount: Optional[Decimal] = None - is_atomic_signing: Optional[bool] = False - label: str = '' - mmp: bool = False - nonce_to_cancel: Optional[int] = None - order_id_to_cancel: Optional[str] = None - order_type: OrderType = 'limit' - reduce_only: bool = False - referral_code: str = '0x9135BA0f495244dc0A5F029b25CDE95157Db89AD' - reject_timestamp: int = 9223372036854776000 - time_in_force: TimeInForce = 'gtc' - trigger_price: Optional[Decimal] = None - trigger_price_type: Optional[TriggerPriceType] = None - trigger_type: Optional[TriggerType] = None - - -class RPCErrorFormatSchema(Struct): - code: int - message: str - data: Optional[str] = None - - -class TxStatus5(str, Enum): - settled = 'settled' - reverted = 'reverted' - timed_out = 'timed_out' - - -class PublicGetTradeHistoryParamsSchema(Struct): - currency: Optional[str] = None - from_timestamp: int = 0 - instrument_name: Optional[str] = None - instrument_type: Optional[InstrumentType] = None - page: int = 1 - page_size: int = 100 - subaccount_id: Optional[int] = None - to_timestamp: int = 18446744073709552000 - trade_id: Optional[str] = None - tx_hash: Optional[str] = None - tx_status: TxStatus5 = 'settled' - - -class TradeSettledPublicResponseSchema(Struct): - direction: Direction - expected_rebate: Decimal - index_price: Decimal - instrument_name: str - liquidity_role: LiquidityRole - mark_price: Decimal - realized_pnl: Decimal - realized_pnl_excl_fees: Decimal - subaccount_id: int - timestamp: int - trade_amount: Decimal - trade_fee: Decimal - trade_id: str - trade_price: Decimal - tx_hash: str - tx_status: TxStatus5 - wallet: str - quote_id: Optional[str] = None - - -class PublicSendQuoteDebugParamsSchema(Struct): - direction: Direction - legs: List[LegPricedSchema] - max_fee: Decimal - nonce: int - rfq_id: str - signature: str - signature_expiry_sec: int - signer: str - subaccount_id: int - label: str = '' - mmp: bool = False - - -class PublicSendQuoteDebugResultSchema(PublicDepositDebugResultSchema): - pass - - -class PrivateGetOrderHistoryParamsSchema(Struct): - subaccount_id: int - page: int = 1 - page_size: int = 100 - - -class PrivateGetOrderHistoryResultSchema(PrivateGetOrdersResultSchema): - pass - - -class PrivateCancelBatchRfqsParamsSchema(Struct): - subaccount_id: int - label: Optional[str] = None - nonce: Optional[int] = None - rfq_id: Optional[str] = None - - -class PrivateCancelBatchRfqsResultSchema(PrivateCancelBatchQuotesResultSchema): - pass - - -class PrivateExecuteQuoteParamsSchema(PublicExecuteQuoteDebugParamsSchema): - pass - - -class PrivateExecuteQuoteResultSchema(Struct): - cancel_reason: CancelReason - creation_timestamp: int - direction: Direction - fee: Decimal - fill_pct: Decimal - is_transfer: bool - label: str - last_update_timestamp: int - legs: List[LegPricedSchema] - legs_hash: str - liquidity_role: LiquidityRole - max_fee: Decimal - mmp: bool - nonce: int - quote_id: str - rfq_filled_pct: Decimal - rfq_id: str - signature: str - signature_expiry_sec: int - signer: str - status: Status - subaccount_id: int - tx_hash: Optional[str] = None - tx_status: Optional[TxStatus] = None - - -class PublicCreateSubaccountDebugParamsSchema(Struct): - amount: Decimal - asset_name: str - margin_type: MarginType - nonce: int - signature_expiry_sec: int - signer: str - wallet: str - currency: Optional[str] = None - - -class PublicCreateSubaccountDebugResultSchema(PublicDepositDebugResultSchema): - pass - - -class PrivateGetLiquidationHistoryParamsSchema(PrivateGetInterestHistoryParamsSchema): - pass - - -class PrivateCancelRfqParamsSchema(Struct): - rfq_id: str - subaccount_id: int - - -class PrivateCancelRfqResponseSchema(PrivateResetMmpResponseSchema): - pass - - -class PublicGetLatestSignedFeedsParamsSchema(Struct): - currency: Optional[str] = None - expiry: Optional[int] = None - - -class OracleSignatureDataSchema(Struct): - signatures: Optional[List[str]] = None - signers: Optional[List[str]] = None - - -class Type(str, Enum): - P = 'P' - A = 'A' - B = 'B' - - -class PerpFeedDataSchema(Struct): - confidence: Decimal - currency: str - deadline: int - signatures: OracleSignatureDataSchema - spot_diff_value: Decimal - timestamp: int - type: Type - - -class RateFeedDataSchema(Struct): - confidence: Decimal - currency: str - deadline: int - expiry: int - rate: Decimal - signatures: OracleSignatureDataSchema - timestamp: int - - -class FeedSourceType(str, Enum): - S = 'S' - O = 'O' - - -class SpotFeedDataSchema(Struct): - confidence: Decimal - currency: str - deadline: int - price: Decimal - signatures: OracleSignatureDataSchema - timestamp: int - feed_source_type: FeedSourceType = 'S' - - -class VolSVIParamDataSchema(Struct): - SVI_a: Decimal - SVI_b: Decimal - SVI_fwd: Decimal - SVI_m: Decimal - SVI_refTau: Decimal - SVI_rho: Decimal - SVI_sigma: Decimal - - -class PublicGetReferralPerformanceParamsSchema(Struct): - end_ms: int - start_ms: int - referral_code: Optional[str] = None - wallet: Optional[str] = None - - -class ReferralPerformanceByInstrumentTypeSchema(Struct): - fee_reward: Decimal - notional_volume: Decimal - referred_fee: Decimal - - -class PrivateCreateSubaccountParamsSchema(Struct): - amount: Decimal - asset_name: str - margin_type: MarginType - nonce: int - signature: str - signature_expiry_sec: int - signer: str - wallet: str - currency: Optional[str] = None - - -class PrivateCreateSubaccountResultSchema(PrivateTransferErc20ResultSchema): - pass - - -class PrivateCancelParamsSchema(Struct): - instrument_name: str - order_id: str - subaccount_id: int - - -class PrivateCancelResultSchema(OrderResponseSchema): - pass - - -class Period1(str, Enum): - field_60 = 60 - field_300 = 300 - field_900 = 900 - field_1800 = 1800 - field_3600 = 3600 - field_14400 = 14400 - field_28800 = 28800 - field_86400 = 86400 - field_604800 = 604800 - - -class PublicGetSpotFeedHistoryCandlesParamsSchema(Struct): - currency: str - end_timestamp: int - period: Period1 - start_timestamp: int - - -class SpotFeedHistoryCandlesResponseSchema(Struct): - close_price: Decimal - high_price: Decimal - low_price: Decimal - open_price: Decimal - price: Decimal - timestamp: int - timestamp_bucket: int - - -class PrivateGetRfqsParamsSchema(Struct): - subaccount_id: int - from_timestamp: int = 0 - page: int = 1 - page_size: int = 100 - rfq_id: Optional[str] = None - status: Optional[Status] = None - to_timestamp: int = 18446744073709552000 - - -class RFQResultSchema(Struct): - cancel_reason: CancelReason - creation_timestamp: int - filled_pct: Decimal - label: str - last_update_timestamp: int - legs: List[LegUnpricedSchema] - partial_fill_step: Decimal - rfq_id: str - status: Status - subaccount_id: int - valid_until: int - ask_total_cost: Optional[Decimal] = None - bid_total_cost: Optional[Decimal] = None - counterparties: Optional[List[str]] = None - filled_direction: Optional[Direction] = None - mark_total_cost: Optional[Decimal] = None - max_total_cost: Optional[Decimal] = None - min_total_cost: Optional[Decimal] = None - total_cost: Optional[Decimal] = None - - -class PrivateCancelByNonceParamsSchema(Struct): - instrument_name: str - nonce: int - subaccount_id: int - wallet: str - - -class PrivateCancelByNonceResultSchema(PrivateCancelByLabelResultSchema): - pass - - -class PrivateGetSubaccountParamsSchema(PrivateGetOpenOrdersParamsSchema): - pass - - -class PrivateSendRfqParamsSchema(Struct): - legs: List[LegUnpricedSchema] - subaccount_id: int - counterparties: Optional[List[str]] = None - label: str = '' - max_total_cost: Optional[Decimal] = None - min_total_cost: Optional[Decimal] = None - partial_fill_step: Decimal = '1' - - -class PrivateSendRfqResultSchema(RFQResultSchema): - pass - - -class PublicGetTimeParamsSchema(PublicGetVaultStatisticsParamsSchema): - pass - - -class PublicGetTimeResponseSchema(Struct): - id: Union[str, int] - result: int - - -class PublicStatisticsParamsSchema(Struct): - instrument_name: str - currency: Optional[str] = None - end_time: Optional[int] = None - - -class PublicStatisticsResultSchema(Struct): - daily_fees: Decimal - daily_notional_volume: Decimal - daily_premium_volume: Decimal - daily_trades: int - open_interest: Decimal - total_fees: Decimal - total_notional_volume: Decimal - total_premium_volume: Decimal - total_trades: int - - -class PublicGetAllInstrumentsParamsSchema(Struct): - expired: bool - instrument_type: InstrumentType - currency: Optional[str] = None - page: int = 1 - page_size: int = 100 - - -class PrivateGetLiquidatorHistoryParamsSchema(Struct): - subaccount_id: int - end_timestamp: int = 9223372036854776000 - page: int = 1 - page_size: int = 100 - start_timestamp: int = 0 - - -class PrivateGetLiquidatorHistoryResultSchema(Struct): - bids: List[AuctionBidEventSchema] - pagination: PaginationInfoSchema | None = None - - -class PublicRegisterSessionKeyParamsSchema(Struct): - expiry_sec: int - label: str - public_session_key: str - signed_raw_tx: str - wallet: str - - -class PublicRegisterSessionKeyResultSchema(Struct): - label: str - public_session_key: str - transaction_id: str - - -class PublicGetVaultBalancesParamsSchema(Struct): - smart_contract_owner: Optional[str] = None - wallet: Optional[str] = None - - -class VaultBalanceResponseSchema(Struct): - address: str - amount: Decimal - chain_id: int - name: str - vault_asset_type: str - - -class PrivateGetAccountParamsSchema(PrivateSessionKeysParamsSchema): - pass - - -class AccountFeeInfoSchema(Struct): - base_fee_discount: Decimal - rfq_maker_discount: Decimal - rfq_taker_discount: Decimal - option_maker_fee: Optional[Decimal] = None - option_taker_fee: Optional[Decimal] = None - perp_maker_fee: Optional[Decimal] = None - perp_taker_fee: Optional[Decimal] = None - spot_maker_fee: Optional[Decimal] = None - spot_taker_fee: Optional[Decimal] = None - - -class PrivateCancelQuoteParamsSchema(Struct): - quote_id: str - subaccount_id: int - - -class PrivateCancelQuoteResultSchema(QuoteResultSchema): - pass - - -class PrivateCancelByInstrumentParamsSchema(Struct): - instrument_name: str - subaccount_id: int - - -class PrivateCancelByInstrumentResultSchema(PrivateCancelByLabelResultSchema): - pass - - -class PrivateSendQuoteParamsSchema(PublicSendQuoteDebugParamsSchema): - pass - - -class PrivateSendQuoteResultSchema(QuoteResultSchema): - pass - - -class PrivateCancelAllParamsSchema(PrivateGetOpenOrdersParamsSchema): - pass - - -class PrivateCancelAllResponseSchema(PrivateResetMmpResponseSchema): - pass - - -class PublicGetVaultStatisticsResponseSchema(Struct): - id: Union[str, int] - result: List[VaultStatisticsResponseSchema] - - -class SignedQuoteParamsSchema(Struct): - direction: Direction - legs: List[LegPricedSchema] - max_fee: Decimal - nonce: int - signature: str - signature_expiry_sec: int - signer: str - subaccount_id: int - - -class PrivateTransferPositionsResultSchema(Struct): - maker_quote: QuoteResultSchema - taker_quote: QuoteResultSchema - - -class PublicGetOptionSettlementPricesResultSchema(Struct): - expiries: List[ExpiryResponseSchema] - - -class PrivateTransferPositionParamsSchema(Struct): - maker_params: TradeModuleParamsSchema - taker_params: TradeModuleParamsSchema - wallet: str - - -class PrivateTransferPositionResultSchema(Struct): - maker_order: OrderResponseSchema - maker_trade: TradeResponseSchema - taker_order: OrderResponseSchema - taker_trade: TradeResponseSchema - - -class PublicDepositDebugResponseSchema(Struct): - id: Union[str, int] - result: PublicDepositDebugResultSchema - - -class PrivateGetOpenOrdersResponseSchema(Struct): - id: Union[str, int] - result: PrivateGetOpenOrdersResultSchema - - -class PrivateTransferErc20ParamsSchema(Struct): - recipient_details: SignatureDetailsSchema - recipient_subaccount_id: int - sender_details: SignatureDetailsSchema - subaccount_id: int - transfer: TransferDetailsSchema - - -class PrivateTransferErc20ResponseSchema(Struct): - id: Union[str, int] - result: PrivateTransferErc20ResultSchema - - -class PrivateRegisterScopedSessionKeyResponseSchema(Struct): - id: Union[str, int] - result: PrivateRegisterScopedSessionKeyResultSchema - - -class PublicGetCurrencyResultSchema(CurrencyDetailedResponseSchema): - pass - - -class PrivateLiquidateResponseSchema(Struct): - id: Union[str, int] - result: PrivateLiquidateResultSchema - - -class PrivateGetSubaccountValueHistoryResultSchema(Struct): - subaccount_id: int - subaccount_value_history: List[SubAccountValueHistoryResponseSchema] - - -class PublicGetMakerProgramsResponseSchema(Struct): - id: Union[str, int] - result: List[ProgramResponseSchema] - - -class SignedTradeOrderSchema(Struct): - data: TradeModuleDataSchema - expiry: int - is_atomic_signing: bool - module: str - nonce: int - owner: str - signature: str - signer: str - subaccount_id: int - - -class PrivateGetInterestHistoryResultSchema(Struct): - events: List[InterestPaymentSchema] - - -class PrivateGetDepositHistoryResultSchema(Struct): - events: List[DepositSchema] - - -class PrivateGetMmpConfigResponseSchema(Struct): - id: Union[str, int] - result: List[MMPConfigResultSchema] - - -class PrivateSessionKeysResultSchema(Struct): - public_session_keys: List[SessionKeyResponseSchema] - - -class InstrumentPublicResponseSchema(PublicGetInstrumentResultSchema): - pass - - -class PrivateGetSubaccountResultSchema(Struct): - collaterals: List[CollateralResponseSchema] - collaterals_initial_margin: Decimal - collaterals_maintenance_margin: Decimal - collaterals_value: Decimal - currency: str - initial_margin: Decimal - is_under_liquidation: bool - label: str - maintenance_margin: Decimal - margin_type: MarginType - open_orders: List[OrderResponseSchema] - open_orders_margin: Decimal - positions: List[PositionResponseSchema] - positions_initial_margin: Decimal - positions_maintenance_margin: Decimal - positions_value: Decimal - projected_margin_change: Decimal - subaccount_id: int - subaccount_value: Decimal - - -class PublicGetInstrumentResponseSchema(Struct): - id: Union[str, int] - result: PublicGetInstrumentResultSchema - - -class PublicExecuteQuoteDebugResponseSchema(Struct): - id: Union[str, int] - result: PublicExecuteQuoteDebugResultSchema - - -class PrivateGetCollateralsResponseSchema(Struct): - id: Union[str, int] - result: PrivateGetCollateralsResultSchema - - -class PrivatePollQuotesResultSchema(Struct): - quotes: List[QuoteResultPublicSchema] - pagination: PaginationInfoSchema | None = None - - -class PrivateGetMarginParamsSchema(Struct): - subaccount_id: int - simulated_collateral_changes: Optional[List[SimulatedCollateralSchema]] = None - simulated_position_changes: Optional[List[SimulatedPositionSchema]] = None - - -class PrivateGetMarginResponseSchema(Struct): - id: Union[str, int] - result: PrivateGetMarginResultSchema - - -class PublicBuildRegisterSessionKeyTxResponseSchema(Struct): - id: Union[str, int] - result: PublicBuildRegisterSessionKeyTxResultSchema - - -class PrivateCancelTriggerOrderResponseSchema(Struct): - id: Union[str, int] - result: PrivateCancelTriggerOrderResultSchema - - -class PrivateGetOrderResponseSchema(Struct): - id: Union[str, int] - result: PrivateGetOrderResultSchema - - -class PrivateGetWithdrawalHistoryResultSchema(Struct): - events: List[WithdrawalSchema] - - -class PublicGetLiveIncidentsResultSchema(Struct): - incidents: List[IncidentResponseSchema] - - -class PrivateGetQuotesResponseSchema(Struct): - id: Union[str, int] - result: PrivateGetQuotesResultSchema - - -class PrivateGetPositionsResponseSchema(Struct): - id: Union[str, int] - result: PrivateGetPositionsResultSchema - - -class PrivateGetOptionSettlementHistoryResultSchema(Struct): - settlements: List[OptionSettlementResponseSchema] - subaccount_id: int - - -class PublicDeregisterSessionKeyResponseSchema(Struct): - id: Union[str, int] - result: PublicDeregisterSessionKeyResultSchema - - -class PublicGetVaultShareResultSchema(Struct): - vault_shares: List[VaultShareResponseSchema] - pagination: PaginationInfoSchema | None = None - - -class PrivateExpiredAndCancelledHistoryResponseSchema(Struct): - id: Union[str, int] - result: PrivateExpiredAndCancelledHistoryResultSchema - - -class PrivateEditSessionKeyResponseSchema(Struct): - id: Union[str, int] - result: PrivateEditSessionKeyResultSchema - - -class PublicGetAllCurrenciesResponseSchema(Struct): - id: Union[str, int] - result: List[CurrencyDetailedResponseSchema] - - -class PrivateCancelByLabelResponseSchema(Struct): - id: Union[str, int] - result: PrivateCancelByLabelResultSchema - - -class PublicWithdrawDebugResponseSchema(Struct): - id: Union[str, int] - result: PublicWithdrawDebugResultSchema - - -class PublicGetMarginResponseSchema(Struct): - id: Union[str, int] - result: PublicGetMarginResultSchema - - -class PrivateGetSubaccountsResponseSchema(Struct): - id: Union[str, int] - result: PrivateGetSubaccountsResultSchema - - -class RFQResultPublicSchema(Struct): - cancel_reason: CancelReason - creation_timestamp: int - filled_pct: Decimal - last_update_timestamp: int - legs: List[LegUnpricedSchema] - partial_fill_step: Decimal - rfq_id: str - status: Status - subaccount_id: int - valid_until: int - filled_direction: Optional[Direction] = None - total_cost: Optional[Decimal] = None - - -class PrivateWithdrawResponseSchema(Struct): - id: Union[str, int] - result: PrivateWithdrawResultSchema - - -class PrivateUpdateNotificationsResponseSchema(Struct): - id: Union[str, int] - result: PrivateUpdateNotificationsResultSchema - - -class PrivateGetTradeHistoryResponseSchema(Struct): - id: Union[str, int] - result: PrivateGetTradeHistoryResultSchema - - -class PrivateOrderResponseSchema(Struct): - id: Union[str, int] - result: PrivateOrderResultSchema - - -class PublicGetInterestRateHistoryResultSchema(Struct): - interest_rates: List[InterestRateHistoryResponseSchema] - pagination: PaginationInfoSchema | None = None - - -class PublicGetOptionSettlementHistoryResponseSchema(Struct): - id: Union[str, int] - result: PublicGetOptionSettlementHistoryResultSchema - - -class PublicGetMakerProgramScoresResultSchema(Struct): - program: ProgramResponseSchema - scores: List[ScoreBreakdownSchema] - total_score: Decimal - total_volume: Decimal - - -class PrivateGetOrdersResponseSchema(Struct): - id: Union[str, int] - result: PrivateGetOrdersResultSchema - - -class PublicGetTickerResultSchema(Struct): - amount_step: Decimal - base_asset_address: str - base_asset_sub_id: str - base_currency: str - base_fee: Decimal - best_ask_amount: Decimal - best_ask_price: Decimal - best_bid_amount: Decimal - best_bid_price: Decimal - fifo_min_allocation: Decimal - five_percent_ask_depth: Decimal - five_percent_bid_depth: Decimal - index_price: Decimal - instrument_name: str - instrument_type: InstrumentType - is_active: bool - maker_fee_rate: Decimal - mark_price: Decimal - max_price: Decimal - maximum_amount: Decimal - min_price: Decimal - minimum_amount: Decimal - open_interest: Dict[str, List[OpenInterestStatsSchema]] - pro_rata_amount_step: Decimal - pro_rata_fraction: Decimal - quote_currency: str - scheduled_activation: int - scheduled_deactivation: int - stats: AggregateTradingStatsSchema - taker_fee_rate: Decimal - tick_size: Decimal - timestamp: int - erc20_details: Optional[ERC20PublicDetailsSchema] = None - option_details: Optional[OptionPublicDetailsSchema] = None - option_pricing: Optional[OptionPricingSchema] = None - perp_details: Optional[PerpPublicDetailsSchema] = None - mark_price_fee_rate_cap: Optional[Decimal] = None - - -class PrivateGetFundingHistoryResultSchema(Struct): - events: List[FundingPaymentSchema] - pagination: PaginationInfoSchema | None = None - - -class PublicGetSpotFeedHistoryResultSchema(Struct): - currency: str - spot_feed_history: List[SpotFeedHistoryResponseSchema] - - -class PrivateSetMmpConfigResponseSchema(Struct): - id: Union[str, int] - result: PrivateSetMmpConfigResultSchema - - -class PublicGetFundingRateHistoryResultSchema(Struct): - funding_rate_history: List[FundingRateSchema] - - -class PrivateGetNotificationsResultSchema(Struct): - notifications: List[NotificationResponseSchema] - pagination: PaginationInfoSchema | None = None - - -class PrivateCancelBatchQuotesResponseSchema(Struct): - id: Union[str, int] - result: PrivateCancelBatchQuotesResultSchema - - -class PrivateRfqGetBestQuoteResponseSchema(Struct): - id: Union[str, int] - result: PrivateRfqGetBestQuoteResultSchema - - -class PrivateDepositResponseSchema(Struct): - id: Union[str, int] - result: PrivateDepositResultSchema - - -class AuctionResultSchema(Struct): - auction_id: str - auction_type: AuctionType - bids: List[AuctionBidEventSchema] - fee: Decimal - start_timestamp: int - subaccount_id: int - tx_hash: str - end_timestamp: Optional[int] = None - - -class PrivateChangeSubaccountLabelResponseSchema(Struct): - id: Union[str, int] - result: PrivateChangeSubaccountLabelResultSchema - - -class PublicMarginWatchResultSchema(Struct): - collaterals: List[CollateralPublicResponseSchema] - currency: str - initial_margin: Decimal - maintenance_margin: Decimal - margin_type: MarginType - positions: List[PositionPublicResponseSchema] - subaccount_id: int - subaccount_value: Decimal - valuation_timestamp: int - - -class PublicGetTransactionResponseSchema(Struct): - id: Union[str, int] - result: PublicGetTransactionResultSchema - - -class PrivateGetErc20TransferHistoryResultSchema(Struct): - events: List[ERC20TransferSchema] - - -class PrivateReplaceResultSchema(Struct): - cancelled_order: OrderResponseSchema - create_order_error: Optional[RPCErrorFormatSchema] = None - order: Optional[OrderResponseSchema] = None - trades: Optional[List[TradeResponseSchema]] = None - - -class PublicGetTradeHistoryResultSchema(Struct): - trades: List[TradeSettledPublicResponseSchema] - pagination: PaginationInfoSchema | None = None - - -class PublicSendQuoteDebugResponseSchema(Struct): - id: Union[str, int] - result: PublicSendQuoteDebugResultSchema - - -class PrivateGetOrderHistoryResponseSchema(Struct): - id: Union[str, int] - result: PrivateGetOrderHistoryResultSchema - - -class PrivateCancelBatchRfqsResponseSchema(Struct): - id: Union[str, int] - result: PrivateCancelBatchRfqsResultSchema - - -class PrivateExecuteQuoteResponseSchema(Struct): - id: Union[str, int] - result: PrivateExecuteQuoteResultSchema - - -class PublicCreateSubaccountDebugResponseSchema(Struct): - id: Union[str, int] - result: PublicCreateSubaccountDebugResultSchema - - -class PrivateGetLiquidationHistoryResponseSchema(Struct): - id: Union[str, int] - result: List[AuctionResultSchema] - - -class ForwardFeedDataSchema(Struct): - confidence: Decimal - currency: str - deadline: int - expiry: int - fwd_diff: Decimal - signatures: OracleSignatureDataSchema - spot_aggregate_latest: Decimal - spot_aggregate_start: Decimal - timestamp: int - - -class VolFeedDataSchema(Struct): - confidence: Decimal - currency: str - deadline: int - expiry: int - signatures: OracleSignatureDataSchema - timestamp: int - vol_data: VolSVIParamDataSchema - - -class PublicGetReferralPerformanceResultSchema(Struct): - fee_share_percentage: Decimal - referral_code: str - rewards: Dict[str, Dict[str, Dict[str, ReferralPerformanceByInstrumentTypeSchema]]] - stdrv_balance: Decimal - total_fee_rewards: Decimal - total_notional_volume: Decimal - total_referred_fees: Decimal - - -class PrivateCreateSubaccountResponseSchema(Struct): - id: Union[str, int] - result: PrivateCreateSubaccountResultSchema - - -class PrivateCancelResponseSchema(Struct): - id: Union[str, int] - result: PrivateCancelResultSchema - - -class PublicGetSpotFeedHistoryCandlesResultSchema(Struct): - currency: str - spot_feed_history: List[SpotFeedHistoryCandlesResponseSchema] - - -class PrivateGetRfqsResultSchema(Struct): - rfqs: List[RFQResultSchema] - pagination: PaginationInfoSchema | None = None - - -class PrivateCancelByNonceResponseSchema(Struct): - id: Union[str, int] - result: PrivateCancelByNonceResultSchema - - -class PrivateGetSubaccountResponseSchema(Struct): - id: Union[str, int] - result: PrivateGetSubaccountResultSchema - - -class PrivateSendRfqResponseSchema(Struct): - id: Union[str, int] - result: PrivateSendRfqResultSchema - - -class PublicStatisticsResponseSchema(Struct): - id: Union[str, int] - result: PublicStatisticsResultSchema - - -class PublicGetAllInstrumentsResultSchema(Struct): - instruments: List[InstrumentPublicResponseSchema] - pagination: PaginationInfoSchema | None = None - - -class PrivateGetLiquidatorHistoryResponseSchema(Struct): - id: Union[str, int] - result: PrivateGetLiquidatorHistoryResultSchema - - -class PublicRegisterSessionKeyResponseSchema(Struct): - id: Union[str, int] - result: PublicRegisterSessionKeyResultSchema - - -class PublicGetVaultBalancesResponseSchema(Struct): - id: Union[str, int] - result: List[VaultBalanceResponseSchema] - - -class PrivateGetAccountResultSchema(Struct): - cancel_on_disconnect: bool - fee_info: AccountFeeInfoSchema - is_rfq_maker: bool - per_endpoint_tps: Dict[str, Any] - subaccount_ids: List[int] - wallet: str - websocket_matching_tps: int - websocket_non_matching_tps: int - websocket_option_tps: int - websocket_perp_tps: int - referral_code: Optional[str] = None - - -class PrivateCancelQuoteResponseSchema(Struct): - id: Union[str, int] - result: PrivateCancelQuoteResultSchema - - -class PrivateCancelByInstrumentResponseSchema(Struct): - id: Union[str, int] - result: PrivateCancelByInstrumentResultSchema - - -class PrivateSendQuoteResponseSchema(Struct): - id: Union[str, int] - result: PrivateSendQuoteResultSchema - - -class PrivateTransferPositionsParamsSchema(Struct): - maker_params: SignedQuoteParamsSchema - taker_params: SignedQuoteParamsSchema - wallet: str - - -class PrivateTransferPositionsResponseSchema(Struct): - id: Union[str, int] - result: PrivateTransferPositionsResultSchema - - -class PublicGetOptionSettlementPricesResponseSchema(Struct): - id: Union[str, int] - result: PublicGetOptionSettlementPricesResultSchema - - -class PrivateTransferPositionResponseSchema(Struct): - id: Union[str, int] - result: PrivateTransferPositionResultSchema - - -class PublicGetCurrencyResponseSchema(Struct): - id: Union[str, int] - result: PublicGetCurrencyResultSchema - - -class PrivateGetSubaccountValueHistoryResponseSchema(Struct): - id: Union[str, int] - result: PrivateGetSubaccountValueHistoryResultSchema - - -class PrivateOrderDebugResultSchema(Struct): - action_hash: str - encoded_data: str - encoded_data_hashed: str - raw_data: SignedTradeOrderSchema - typed_data_hash: str - - -class PrivateGetInterestHistoryResponseSchema(Struct): - id: Union[str, int] - result: PrivateGetInterestHistoryResultSchema - - -class PrivateGetDepositHistoryResponseSchema(Struct): - id: Union[str, int] - result: PrivateGetDepositHistoryResultSchema - - -class PrivateSessionKeysResponseSchema(Struct): - id: Union[str, int] - result: PrivateSessionKeysResultSchema - - -class PublicGetInstrumentsResponseSchema(Struct): - id: Union[str, int] - result: List[InstrumentPublicResponseSchema] - - -class PrivateGetAllPortfoliosResponseSchema(Struct): - id: Union[str, int] - result: List[PrivateGetSubaccountResultSchema] - - -class PrivatePollQuotesResponseSchema(Struct): - id: Union[str, int] - result: PrivatePollQuotesResultSchema - - -class PrivateGetWithdrawalHistoryResponseSchema(Struct): - id: Union[str, int] - result: PrivateGetWithdrawalHistoryResultSchema - - -class PublicGetLiveIncidentsResponseSchema(Struct): - id: Union[str, int] - result: PublicGetLiveIncidentsResultSchema - - -class PrivateGetOptionSettlementHistoryResponseSchema(Struct): - id: Union[str, int] - result: PrivateGetOptionSettlementHistoryResultSchema - - -class PublicGetVaultShareResponseSchema(Struct): - id: Union[str, int] - result: PublicGetVaultShareResultSchema - - -class PrivatePollRfqsResultSchema(Struct): - rfqs: List[RFQResultPublicSchema] - pagination: PaginationInfoSchema | None = None - - -class PublicGetInterestRateHistoryResponseSchema(Struct): - id: Union[str, int] - result: PublicGetInterestRateHistoryResultSchema - - -class PublicGetMakerProgramScoresResponseSchema(Struct): - id: Union[str, int] - result: PublicGetMakerProgramScoresResultSchema - - -class PublicGetTickerResponseSchema(Struct): - id: Union[str, int] - result: PublicGetTickerResultSchema - - -class PrivateGetFundingHistoryResponseSchema(Struct): - id: Union[str, int] - result: PrivateGetFundingHistoryResultSchema - - -class PublicGetSpotFeedHistoryResponseSchema(Struct): - id: Union[str, int] - result: PublicGetSpotFeedHistoryResultSchema - - -class PublicGetFundingRateHistoryResponseSchema(Struct): - id: Union[str, int] - result: PublicGetFundingRateHistoryResultSchema - - -class PrivateGetNotificationsResponseSchema(Struct): - id: Union[str, int] - result: PrivateGetNotificationsResultSchema - - -class PublicGetLiquidationHistoryResultSchema(Struct): - auctions: List[AuctionResultSchema] - pagination: PaginationInfoSchema | None = None - - -class PublicMarginWatchResponseSchema(Struct): - id: Union[str, int] - result: PublicMarginWatchResultSchema - - -class PrivateGetErc20TransferHistoryResponseSchema(Struct): - id: Union[str, int] - result: PrivateGetErc20TransferHistoryResultSchema - - -class PrivateReplaceResponseSchema(Struct): - id: Union[str, int] - result: PrivateReplaceResultSchema - - -class PublicGetTradeHistoryResponseSchema(Struct): - id: Union[str, int] - result: PublicGetTradeHistoryResultSchema - - -class PublicGetLatestSignedFeedsResultSchema(Struct): - fwd_data: Dict[str, Dict[str, ForwardFeedDataSchema]] - perp_data: Dict[str, Dict[str, PerpFeedDataSchema]] - rate_data: Dict[str, Dict[str, RateFeedDataSchema]] - spot_data: Dict[str, SpotFeedDataSchema] - vol_data: Dict[str, Dict[str, VolFeedDataSchema]] - - -class PublicGetReferralPerformanceResponseSchema(Struct): - id: Union[str, int] - result: PublicGetReferralPerformanceResultSchema - - -class PublicGetSpotFeedHistoryCandlesResponseSchema(Struct): - id: Union[str, int] - result: PublicGetSpotFeedHistoryCandlesResultSchema - - -class PrivateGetRfqsResponseSchema(Struct): - id: Union[str, int] - result: PrivateGetRfqsResultSchema - - -class PublicGetAllInstrumentsResponseSchema(Struct): - id: Union[str, int] - result: PublicGetAllInstrumentsResultSchema - - -class PrivateGetAccountResponseSchema(Struct): - id: Union[str, int] - result: PrivateGetAccountResultSchema - - -class PrivateOrderDebugResponseSchema(Struct): - id: Union[str, int] - result: PrivateOrderDebugResultSchema - - -class PrivatePollRfqsResponseSchema(Struct): - id: Union[str, int] - result: PrivatePollRfqsResultSchema - - -class PublicGetLiquidationHistoryResponseSchema(Struct): - id: Union[str, int] - result: PublicGetLiquidationHistoryResultSchema - - -class PublicGetLatestSignedFeedsResponseSchema(Struct): - id: Union[str, int] - result: PublicGetLatestSignedFeedsResultSchema diff --git a/derive_client/data/templates/api.py.jinja b/derive_client/data/templates/api.py.jinja index bbb96042..e88edad6 100644 --- a/derive_client/data/templates/api.py.jinja +++ b/derive_client/data/templates/api.py.jinja @@ -3,10 +3,10 @@ import msgspec from derive_client._clients.rest{% if is_async %}.async_http{% else %}.http{% endif %}.session import {% if is_async %}AsyncHTTPSession{% else %}HTTPSession{% endif %} -from derive_client.constants import EnvConfig, PUBLIC_HEADERS +from derive_client.config import EnvConfig, PUBLIC_HEADERS from derive_client._clients.utils import try_cast_response, AuthContext, encode_json_exclude_none from derive_client._clients.rest.endpoints import PublicEndpoints, PrivateEndpoints -from derive_client.data.generated.models import ( +from derive_client.data_types.generated_models import ( {% for schema in schema_imports -%} {{ schema }}, {% endfor %} diff --git a/derive_client/data_types/__init__.py b/derive_client/data_types/__init__.py index afb2b1b8..cc92c097 100644 --- a/derive_client/data_types/__init__.py +++ b/derive_client/data_types/__init__.py @@ -3,6 +3,7 @@ from .enums import ( BridgeDirection, BridgeType, + ChainID, Currency, DeriveJSONRPCErrorCode, Environment, @@ -42,6 +43,7 @@ __all__ = [ "ChecksumAddress", + "ChainID", "TxStatus", "Direction", "BridgeDirection", diff --git a/derive_client/data_types/enums.py b/derive_client/data_types/enums.py index 761051b2..2ffc75ba 100644 --- a/derive_client/data_types/enums.py +++ b/derive_client/data_types/enums.py @@ -3,6 +3,24 @@ from enum import Enum, IntEnum, StrEnum +class ChainID(IntEnum): + ETH = 1 + OPTIMISM = 10 + DERIVE = LYRA = 957 + BASE = 8453 + MODE = 34443 + ARBITRUM = 42161 + BLAST = 81457 + + @classmethod + def _missing_(cls, value): + try: + int_value = int(value) + return next(member for member in cls if member == int_value) + except (ValueError, TypeError, StopIteration): + return super()._missing_(value) + + class TxStatus(IntEnum): FAILED = 0 # confirmed and status == 0 (on-chain revert) SUCCESS = 1 # confirmed and status == 1 diff --git a/derive_client/data_types/generated_models.py b/derive_client/data_types/generated_models.py index 60c55bdb..0afce1d7 100644 --- a/derive_client/data_types/generated_models.py +++ b/derive_client/data_types/generated_models.py @@ -1139,7 +1139,7 @@ class Period(str, Enum): class PublicGetFundingRateHistoryParamsSchema(Struct): instrument_name: str end_timestamp: int = 9223372036854776000 - period: Period = Period(3600) + period: Period = Period.field_3600 start_timestamp: int = 0 diff --git a/derive_client/data_types/models.py b/derive_client/data_types/models.py index e743c33a..80a28026 100644 --- a/derive_client/data_types/models.py +++ b/derive_client/data_types/models.py @@ -15,9 +15,12 @@ BaseModel, ConfigDict, Field, + GetCoreSchemaHandler, + GetJsonSchemaHandler, HttpUrl, RootModel, ) +from pydantic_core import core_schema from web3 import AsyncWeb3 from web3.contract.async_contract import AsyncContract, AsyncContractEvent from web3.types import ChecksumAddress as ETHChecksumAddress @@ -26,15 +29,43 @@ from derive_client.exceptions import TxReceiptMissing -if TYPE_CHECKING: - from derive_client.constants import ChainID +from .enums import ( + BridgeType, + ChainID, + Currency, + GasPriority, + TxStatus, +) + + +class PHexBytes(HexBytes): + @classmethod + def __get_pydantic_core_schema__(cls, _source: Any, _handler: Any) -> core_schema.CoreSchema: + # Allow either HexBytes or bytes/hex strings to be parsed into HexBytes + return core_schema.no_info_before_validator_function( + cls._validate, + core_schema.union_schema( + [ + core_schema.is_instance_schema(HexBytes), + core_schema.bytes_schema(), + core_schema.str_schema(), + ] + ), + ) + + @classmethod + def __get_pydantic_json_schema__(cls, _schema: core_schema.CoreSchema, _handler: Any) -> dict: + return {"type": "string", "format": "hex"} - from .enums import ( - BridgeType, - Currency, - GasPriority, - TxStatus, - ) + @classmethod + def _validate(cls, v: Any) -> HexBytes: + if isinstance(v, HexBytes): + return v + if isinstance(v, (bytes, bytearray)): + return HexBytes(v) + if isinstance(v, str): + return HexBytes(v) + raise TypeError(f"Expected HexBytes-compatible type, got {type(v).__name__}") class ChecksumAddress(str): @@ -45,6 +76,22 @@ def __new__(cls, v: str) -> ChecksumAddress: raise ValueError(f"Invalid Ethereum address: {v}") return cast(ChecksumAddress, to_checksum_address(v)) + @classmethod + def __get_pydantic_core_schema__(cls, _source, _handler: GetCoreSchemaHandler) -> core_schema.CoreSchema: + return core_schema.no_info_before_validator_function(cls._validate, core_schema.any_schema()) + + @classmethod + def __get_pydantic_json_schema__(cls, _schema, _handler: GetJsonSchemaHandler) -> dict: + return {"type": "string", "format": "ethereum-address"} + + @classmethod + def _validate(cls, v) -> ChecksumAddress: + if isinstance(v, cls): + return v + if not isinstance(v, str): + raise TypeError(f"Expected str, got {type(v)}") + return cls(v) + class TxHash(str): """Transaction hash with validation.""" @@ -58,6 +105,24 @@ def __new__(cls, value: str | HexBytes) -> TxHash: raise ValueError(f"Invalid transaction hash: {value}") return cast(TxHash, value) + @classmethod + def __get_pydantic_core_schema__(cls, _source, _handler: GetCoreSchemaHandler): + return core_schema.no_info_before_validator_function(cls._validate, core_schema.str_schema()) + + @classmethod + def __get_pydantic_json_schema__(cls, _schema, _handler: GetJsonSchemaHandler): + return {"type": "string", "format": "ethereum-tx-hash"} + + @classmethod + def _validate(cls, v: str | HexBytes) -> str: + if isinstance(v, HexBytes): + v = v.to_0x_hex() + if not isinstance(v, str): + raise TypeError("Expected a string or HexBytes for TxHash") + if not is_0x_prefixed(v) or not is_hex(v) or len(v) != 66: + raise ValueError(f"Invalid Ethereum transaction hash: {v}") + return v + class Wei(int): """Wei with validation.""" @@ -67,6 +132,22 @@ def __new__(cls, value: str | int) -> Wei: value = int(value, 16) return cast(Wei, value) + @classmethod + def __get_pydantic_core_schema__(cls, _source, _handler: GetCoreSchemaHandler) -> core_schema.CoreSchema: + return core_schema.no_info_before_validator_function(cls._validate, core_schema.int_schema()) + + @classmethod + def __get_pydantic_json_schema__(cls, _schema, _handler: GetJsonSchemaHandler) -> dict: + return {"type": ["string", "integer"], "title": "Wei"} + + @classmethod + def _validate(cls, v: str | int) -> int: + if isinstance(v, int): + return v + if isinstance(v, str) and is_hex(v): + return int(v, 16) + raise TypeError(f"Invalid type for Wei: {type(v)}") + class TypedFilterParams(BaseModel): """Typed filter params for eth_getLogs that we actually use. @@ -81,13 +162,13 @@ class TypedFilterParams(BaseModel): model_config = ConfigDict(frozen=True) address: ChecksumAddress | list[ChecksumAddress] - topics: tuple[HexBytes | None, ...] | None = None + topics: tuple[PHexBytes | None, ...] | None = None # Block range - we use int internally, convert to hex for RPC # 'latest' is used as sentinel for open-ended queries fromBlock: int | Literal["latest"] toBlock: int | Literal["latest"] - blockHash: HexBytes | None = None + blockHash: PHexBytes | None = None def to_rpc_params(self) -> FilterParams: """Convert to RPC-compatible filter params with hex block numbers.""" @@ -119,13 +200,13 @@ class TypedLogReceipt(BaseModel): """Typed log entry from transaction receipt.""" address: ChecksumAddress - blockHash: HexBytes + blockHash: PHexBytes blockNumber: int - data: HexBytes + data: PHexBytes logIndex: int removed: bool - topics: list[HexBytes] - transactionHash: HexBytes + topics: list[PHexBytes] + transactionHash: PHexBytes transactionIndex: int def to_w3(self) -> LogReceipt: @@ -153,7 +234,7 @@ class TypedTxReceipt(BaseModel): model_config = ConfigDict(populate_by_name=True) - blockHash: HexBytes + blockHash: PHexBytes blockNumber: int contractAddress: ChecksumAddress | None cumulativeGasUsed: int @@ -161,10 +242,10 @@ class TypedTxReceipt(BaseModel): from_: ChecksumAddress = Field(alias='from') gasUsed: int logs: list[TypedLogReceipt] - logsBloom: HexBytes + logsBloom: PHexBytes status: int # 0 or 1 per EIP-658 to: ChecksumAddress - transactionHash: HexBytes + transactionHash: PHexBytes transactionIndex: int type: int = Field(alias='type') # Transaction type (0=legacy, 1=EIP-2930, 2=EIP-1559) @@ -205,8 +286,8 @@ class TypedSignedTransaction(BaseModel): model_config = ConfigDict(frozen=True) - raw_transaction: HexBytes - hash: HexBytes + raw_transaction: PHexBytes + hash: PHexBytes r: int s: int v: int @@ -233,15 +314,15 @@ class TypedTransaction(BaseModel): model_config = ConfigDict(populate_by_name=True) - blockHash: HexBytes | None + blockHash: PHexBytes | None blockNumber: int | None # None if pending from_: ChecksumAddress = Field(alias='from') gas: int gasPrice: int | None = None # Legacy transactions maxFeePerGas: int | None = None # EIP-1559 maxPriorityFeePerGas: int | None = None # EIP-1559 - hash: HexBytes - input: HexBytes + hash: PHexBytes + input: PHexBytes nonce: int to: ChecksumAddress | None # None for contract creation transactionIndex: int | None # None if pending @@ -249,15 +330,15 @@ class TypedTransaction(BaseModel): type: int # 0=legacy, 1=EIP-2930, 2=EIP-1559 chainId: int | None = None v: int - r: HexBytes - s: HexBytes + r: PHexBytes + s: PHexBytes # EIP-2930 (optional) accessList: list[dict[str, Any]] | None = None # EIP-4844 (optional) maxFeePerBlobGas: int | None = None - blobVersionedHashes: list[HexBytes] | None = None + blobVersionedHashes: list[PHexBytes] | None = None class TokenData(BaseModel): @@ -317,12 +398,12 @@ def nonce(self) -> int: return self.tx["nonce"] @property - def gas(self) -> Wei: + def gas(self) -> int: """Gas limit""" return self.tx["gas"] @property - def max_fee_per_gas(self) -> Wei: + def max_fee_per_gas(self) -> int: return self.tx["maxFeePerGas"] @@ -364,7 +445,7 @@ def nonce(self) -> int: return self.tx_details.nonce @property - def gas(self) -> Wei: + def gas(self) -> int: return self.tx_details.gas @property @@ -480,3 +561,6 @@ class PositionTransfer: instrument_name: str amount: Decimal # Can be negative (sign indicates long/short) + + +DeriveAddresses.model_rebuild() From a3355149d08ac72fa5f6bf4d5e09bc3134f010c1 Mon Sep 17 00:00:00 2001 From: zarathustra Date: Sun, 2 Nov 2025 17:07:20 +0100 Subject: [PATCH 15/22] fix: typing utils, just an ordinary day! --- derive_client/utils/__init__.py | 4 - derive_client/utils/abi.py | 108 ------------------------ derive_client/utils/asyncio_sync.py | 116 +++++++++++++++++--------- derive_client/utils/prod_addresses.py | 7 +- derive_client/utils/retry.py | 58 ++++++++----- derive_client/utils/unwrap.py | 4 +- derive_client/utils/w3.py | 35 +++++--- 7 files changed, 138 insertions(+), 194 deletions(-) delete mode 100644 derive_client/utils/abi.py diff --git a/derive_client/utils/__init__.py b/derive_client/utils/__init__.py index daf6005b..db6aefb4 100644 --- a/derive_client/utils/__init__.py +++ b/derive_client/utils/__init__.py @@ -1,7 +1,5 @@ """Utils for the Derive Client package.""" -from .abi import download_prod_address_abis -from .fees import rfq_max_fee from .logger import get_logger from .prod_addresses import get_prod_derive_addresses from .retry import exp_backoff_retry, get_retry_session, wait_until @@ -18,7 +16,5 @@ "load_rpc_endpoints", "to_base_units", "from_base_units", - "download_prod_address_abis", "unwrap_or_raise", - "rfq_max_fee", ] diff --git a/derive_client/utils/abi.py b/derive_client/utils/abi.py deleted file mode 100644 index accd520a..00000000 --- a/derive_client/utils/abi.py +++ /dev/null @@ -1,108 +0,0 @@ -import json - -from web3 import Web3 - -from derive_client.constants import ABI_DATA_DIR -from derive_client.data_types import ChainID, Currency, MintableTokenData, NonMintableTokenData -from derive_client.utils.logger import get_logger -from derive_client.utils.prod_addresses import get_prod_derive_addresses -from derive_client.utils.retry import get_retry_session -from derive_client.utils.w3 import get_w3_connection - -TIMEOUT = 10 -EIP1967_SLOT = (int.from_bytes(Web3.keccak(text="eip1967.proxy.implementation")[:32], "big") - 1).to_bytes(32, "big") - - -CHAIN_ID_TO_URL = { - ChainID.ETH: "https://abidata.net/{address}", - ChainID.OPTIMISM: "https://abidata.net/{address}?network=optimism", - ChainID.ARBITRUM: "https://abidata.net/{address}?network=arbitrum", - ChainID.BASE: "https://abidata.net/{address}?network=base", - ChainID.DERIVE: "https://explorer.derive.xyz/api?module=contract&action=getabi&address={address}", -} - - -def _get_abi(chain_id, contract_address: str): - url = CHAIN_ID_TO_URL[chain_id].format(address=contract_address) - session = get_retry_session() - response = session.get(url, timeout=TIMEOUT) - response.raise_for_status() - if chain_id == ChainID.DERIVE: - return json.loads(response.json()["result"]) - return response.json()["abi"] - - -def _collect_prod_addresses( - currencies: dict[Currency, NonMintableTokenData | MintableTokenData], -): - contract_addresses = [] - for currency, token_data in currencies.items(): - if isinstance(token_data, MintableTokenData): - contract_addresses.append(token_data.Controller) - contract_addresses.append(token_data.MintableToken) - else: # NonMintableTokenData - contract_addresses.append(token_data.Vault) - contract_addresses.append(token_data.NonMintableToken) - - if token_data.LyraTSADepositHook is not None: - contract_addresses.append(token_data.LyraTSADepositHook) - if token_data.LyraTSAShareHandlerDepositHook is not None: - contract_addresses.append(token_data.LyraTSAShareHandlerDepositHook) - for connector_chain_id, connectors in token_data.connectors.items(): - contract_addresses.append(connectors["FAST"]) - return contract_addresses - - -def get_impl_address(w3: Web3, address: str) -> str | None: - """Get EIP1967 Proxy implementation address""" - - data = w3.eth.get_storage_at(address, EIP1967_SLOT) - impl_address = Web3.to_checksum_address(data[-20:]) - if int(impl_address, 16) == 0: - return - return impl_address - - -def download_prod_address_abis(): - """Download Derive production addresses ABIs.""" - - logger = get_logger() - prod_addresses = get_prod_derive_addresses() - - chain_addresses = {} - for chain_id, currencies in prod_addresses.chains.items(): - chain_addresses[chain_id] = _collect_prod_addresses(currencies) - - failures = [] - abi_path = ABI_DATA_DIR.parent / "abis" - - for chain_id, addresses in chain_addresses.items(): - proxy_mapping = {} - w3 = get_w3_connection(chain_id=chain_id) - - if chain_id not in CHAIN_ID_TO_URL: - logger.info(f"Network not supported by abidata.net: {chain_id.name}") - continue - - while addresses: - address = addresses.pop() - if impl_address := get_impl_address(w3=w3, address=address): - logger.info(f"EIP1967 Proxy implementation found: {address} -> {impl_address}") - addresses.append(impl_address) - proxy_mapping[address] = impl_address - try: - abi = _get_abi(chain_id=chain_id, contract_address=address) - except Exception as e: - failures.append(f"{chain_id.name}: {address}: {e}") - continue - - contract_abi_path = abi_path / chain_id.name.lower() / f"{address}.json" - contract_abi_path.parent.mkdir(exist_ok=True, parents=True) - contract_abi_path.write_text(json.dumps(abi, indent=4)) - - proxy_mapping_path = abi_path / chain_id.name.lower() / "proxy_mapping.json" - proxy_mapping_path.write_text(json.dumps(proxy_mapping, indent=4)) - - if failures: - unattained = "\n".join(failures) - logger.error(f"Failed to fetch:\n{unattained}") diff --git a/derive_client/utils/asyncio_sync.py b/derive_client/utils/asyncio_sync.py index 09add7d5..c3652aca 100644 --- a/derive_client/utils/asyncio_sync.py +++ b/derive_client/utils/asyncio_sync.py @@ -1,65 +1,99 @@ import asyncio import inspect import threading +from collections.abc import Awaitable, Coroutine from concurrent.futures import TimeoutError as _TimeoutError -from typing import Any, Optional +from typing import TypeVar -_bg = {"loop": None, "thread": None, "started": False, "start_ev": threading.Event()} -_start_lock = threading.Lock() +T = TypeVar('T') -def _start_bg_loop() -> None: - if _bg["loop"] is not None and _bg["started"]: - return - with _start_lock: - if _bg["loop"] is not None and _bg["started"]: +class _BackgroundLoop: + """Manages a singleton background event loop in a daemon thread.""" + + def __init__(self) -> None: + self._loop: asyncio.AbstractEventLoop | None = None + self._thread: threading.Thread | None = None + self._started: bool = False + self._start_event = threading.Event() + self._start_lock = threading.Lock() + + def start(self) -> None: + """Start the background loop if not already running.""" + if self._loop is not None and self._started: return - def _run() -> None: - loop = asyncio.new_event_loop() - _bg["loop"] = loop - asyncio.set_event_loop(loop) - _bg["start_ev"].set() - _bg["started"] = True - try: - loop.run_forever() - finally: + with self._start_lock: + if self._loop is not None and self._started: + return + + def _run() -> None: + loop = asyncio.new_event_loop() + self._loop = loop + asyncio.set_event_loop(loop) + self._start_event.set() + self._started = True try: - pending = asyncio.all_tasks(loop=loop) - for t in pending: - t.cancel() - loop.run_until_complete(loop.shutdown_asyncgens()) + loop.run_forever() finally: - loop.close() - _bg["loop"] = None - _bg["started"] = False - _bg["start_ev"].clear() + try: + pending = asyncio.all_tasks(loop=loop) + for task in pending: + task.cancel() + loop.run_until_complete(loop.shutdown_asyncgens()) + finally: + loop.close() + self._loop = None + self._started = False + self._start_event.clear() + + thread = threading.Thread(target=_run, name="bg-async-loop", daemon=True) + thread.start() + self._thread = thread + + if not self._start_event.wait(timeout=5): + raise RuntimeError("Failed to start background loop within 5 seconds") - t = threading.Thread(target=_run, name="bg-async-loop", daemon=True) - t.start() - _bg["thread"] = t - _bg["start_ev"].wait(timeout=5) - if not _bg["started"]: - raise RuntimeError("Failed to start background loop") + @property + def loop(self) -> asyncio.AbstractEventLoop: + """Get the background event loop, starting it if necessary.""" + self.start() + if self._loop is None: + raise RuntimeError("Background loop failed to start") + return self._loop -def run_coroutine_sync(coro: object, timeout: Optional[float] = None) -> Any: - """Run coroutine on the single background loop and block until result.""" +# Singleton instance +_bg_loop = _BackgroundLoop() - _start_bg_loop() - loop = _bg["loop"] + +def run_coroutine_sync(coro: Coroutine[None, None, T] | Awaitable[T], timeout: float | None = None) -> T: + """ + Run a coroutine on the background event loop and block until result. + + Args: + coro: A coroutine or awaitable to execute + timeout: Optional timeout in seconds + + Returns: + The result of the coroutine + + Raises: + TimeoutError: If the operation times out + """ + loop = _bg_loop.loop if inspect.iscoroutine(coro): - fut = asyncio.run_coroutine_threadsafe(coro, loop) + future = asyncio.run_coroutine_threadsafe(coro, loop) else: - - async def _await_it(): + # Handle other awaitables + async def _await_it() -> T: return await coro - fut = asyncio.run_coroutine_threadsafe(_await_it(), loop) + future = asyncio.run_coroutine_threadsafe(_await_it(), loop) try: - return fut.result(timeout) + return future.result(timeout) except _TimeoutError: - fut.cancel() + future.cancel() raise diff --git a/derive_client/utils/prod_addresses.py b/derive_client/utils/prod_addresses.py index 3a158e40..d9555f76 100644 --- a/derive_client/utils/prod_addresses.py +++ b/derive_client/utils/prod_addresses.py @@ -1,7 +1,8 @@ +from __future__ import annotations + import json -from collections import defaultdict -from derive_client.constants import DATA_DIR +from derive_client.config import DATA_DIR from derive_client.data_types import DeriveAddresses @@ -9,7 +10,7 @@ def get_prod_derive_addresses() -> DeriveAddresses: """Fetch the socket superbridge JSON data.""" prod_lyra_addresses = DATA_DIR / "prod_lyra_addresses.json" old_prod_lyra_addresses = DATA_DIR / "prod_lyra-old_addresses.json" - chains = defaultdict(dict, {}) + chains = {} for chain_id, data in json.loads(prod_lyra_addresses.read_text()).items(): chain_data = {} for currency, item in data.items(): diff --git a/derive_client/utils/retry.py b/derive_client/utils/retry.py index c80d9083..4c0aa901 100644 --- a/derive_client/utils/retry.py +++ b/derive_client/utils/retry.py @@ -1,14 +1,11 @@ import asyncio import functools import time -from http import HTTPStatus from logging import Logger -from typing import Callable, ParamSpec, Sequence, TypeVar +from typing import Awaitable, Callable, Optional, ParamSpec, Sequence, TypeVar, overload import requests from requests.adapters import HTTPAdapter -from requests.exceptions import ConnectionError as ReqConnectionError -from requests.exceptions import ConnectTimeout, ReadTimeout, RequestException from urllib3.util.retry import Retry from derive_client.utils.logger import get_logger @@ -16,27 +13,46 @@ P = ParamSpec('P') T = TypeVar('T') -RETRY_STATUS_CODES = {HTTPStatus.REQUEST_TIMEOUT, HTTPStatus.TOO_MANY_REQUESTS} | set(range(500, 600)) -RETRY_EXCEPTIONS = ( - ReadTimeout, - ConnectTimeout, - ReqConnectionError, -) +@overload +def exp_backoff_retry(func: Callable[P, Awaitable[T]]) -> Callable[P, Awaitable[T]]: ... +@overload def exp_backoff_retry( - func: Callable[..., T] | None = None, + *, + attempts: int = ..., + initial_delay: float = ..., + exceptions: tuple[type[BaseException], ...] = ..., +) -> Callable[[Callable[P, Awaitable[T]]], Callable[P, Awaitable[T]]]: ... + + +@overload +def exp_backoff_retry( + func: Callable[P, Awaitable[T]], + *, + attempts: int = ..., + initial_delay: float = ..., + exceptions: tuple[type[BaseException], ...] = ..., +) -> Callable[P, Awaitable[T]]: ... + + +def exp_backoff_retry( + func: Optional[Callable[P, Awaitable[T]]] = None, *, attempts: int = 3, initial_delay: float = 1.0, - exceptions=(Exception,), -) -> T: + exceptions: tuple[type[BaseException], ...] = (Exception,), +) -> Callable[P, Awaitable[T]] | Callable[[Callable[P, Awaitable[T]]], Callable[P, Awaitable[T]]]: if func is None: - return lambda f: exp_backoff_retry(f, attempts=attempts, initial_delay=initial_delay, exceptions=exceptions) + + def _decorator(f: Callable[P, Awaitable[T]]) -> Callable[P, Awaitable[T]]: + return exp_backoff_retry(f, attempts=attempts, initial_delay=initial_delay, exceptions=exceptions) + + return _decorator @functools.wraps(func) - async def wrapper(*args, **kwargs): + async def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: delay = initial_delay for attempt in range(attempts): try: @@ -47,6 +63,8 @@ async def wrapper(*args, **kwargs): await asyncio.sleep(delay) delay *= 2 + raise RuntimeError("Should never reach here") + return wrapper @@ -98,13 +116,14 @@ def wait_until( retry_exceptions: type[Exception] | tuple[type[Exception], ...] = (ConnectionError, TimeoutError), max_retries: int = 3, timeout_message: str = "", + *args: P.args, **kwargs: P.kwargs, ) -> T: retries = 0 start_time = time.time() while True: try: - result = func(**kwargs) + result = func(*args, **kwargs) except retry_exceptions: retries += 1 if retries >= max_retries: @@ -117,10 +136,3 @@ def wait_until( msg = f"Timed out after {timeout}s waiting for condition on {func.__name__} {timeout_message}" raise TimeoutError(msg) time.sleep(poll_interval) - - -def is_retryable(e: RequestException) -> bool: - status = getattr(e.response, "status_code", None) - if status in RETRY_STATUS_CODES: - return True - return bool(isinstance(e, RETRY_EXCEPTIONS)) diff --git a/derive_client/utils/unwrap.py b/derive_client/utils/unwrap.py index bb299e94..dc911596 100644 --- a/derive_client/utils/unwrap.py +++ b/derive_client/utils/unwrap.py @@ -16,8 +16,8 @@ def unwrap_or_raise(result: Result[T, Exception] | IOResult[T, Exception]) -> T: case Failure(): raise result.failure() case IOSuccess(): - return unsafe_perform_io(result).unwrap() + return unsafe_perform_io(result.unwrap()) case IOFailure(): - raise unsafe_perform_io(result).failure() + raise unsafe_perform_io(result.failure()) case _: raise RuntimeError(f"unwrap_or_raise received a non-Result value: {result}") diff --git a/derive_client/utils/w3.py b/derive_client/utils/w3.py index b351510e..85f229e9 100644 --- a/derive_client/utils/w3.py +++ b/derive_client/utils/w3.py @@ -9,10 +9,10 @@ import yaml from requests import RequestException -from web3 import Web3 -from web3.providers.rpc import HTTPProvider +from web3 import AsyncHTTPProvider, Web3 +from web3.types import RPCEndpoint, RPCResponse -from derive_client.constants import CURRENCY_DECIMALS, DEFAULT_RPC_ENDPOINTS +from derive_client.config import CURRENCY_DECIMALS, DEFAULT_RPC_ENDPOINTS from derive_client.data_types import ChainID, Currency, RPCEndpoints from derive_client.exceptions import NoAvailableRPC from derive_client.utils.logger import get_logger @@ -21,7 +21,7 @@ class EndpointState: __slots__ = ("provider", "backoff", "next_available") - def __init__(self, provider: HTTPProvider): + def __init__(self, provider: AsyncHTTPProvider): self.provider = provider self.backoff = 0.0 self.next_available = 0.0 @@ -34,12 +34,12 @@ def __str__(self) -> str: def make_rotating_provider_middleware( - endpoints: list[HTTPProvider], + endpoints: list[AsyncHTTPProvider], *, initial_backoff: float = 1.0, max_backoff: float = 600.0, logger: Logger, -) -> Callable[[Callable[[str, Any], Any], Web3], Callable[[str, Any], Any]]: +) -> Callable[[Callable[[RPCEndpoint, Any], RPCResponse], Web3], Callable[[RPCEndpoint, Any], RPCResponse]]: """ v6.11-style middleware: - round-robin via a min-heap of `next_available` times @@ -50,8 +50,11 @@ def make_rotating_provider_middleware( heapq.heapify(heap) lock = threading.Lock() - def middleware_factory(make_request: Callable[[str, Any], Any], w3: Web3) -> Callable[[str, Any], Any]: - def rotating_backoff(method: str, params: Any) -> Any: + def middleware_factory( + make_request: Callable[[RPCEndpoint, Any], RPCResponse], + w3: Web3, + ) -> Callable[[RPCEndpoint, Any], Any]: + def rotating_backoff(method: RPCEndpoint, params: Any) -> Any: now = time.monotonic() while True: @@ -94,10 +97,16 @@ def rotating_backoff(method: str, params: Any) -> Any: logger.debug("Endpoint %s failed: %s", state.provider.endpoint_uri, e) # We retry on all exceptions - hdr = (e.response and e.response.headers or {}).get("Retry-After") - try: - backoff = float(hdr) - except (ValueError, TypeError): + retry_after_header = None + if e.response is not None and e.response.headers is not None: + retry_after_header = e.response.headers.get("Retry-After") + + if retry_after_header is not None and isinstance(retry_after_header, str): + try: + backoff = float(retry_after_header) + except (ValueError, TypeError): + backoff = state.backoff * 2 if state.backoff > 0 else initial_backoff + else: backoff = state.backoff * 2 if state.backoff > 0 else initial_backoff # cap backoff and schedule @@ -134,7 +143,7 @@ def get_w3_connection( logger: Logger | None = None, ) -> Web3: rpc_endpoints = rpc_endpoints or load_rpc_endpoints(DEFAULT_RPC_ENDPOINTS) - providers = [HTTPProvider(url) for url in rpc_endpoints[chain_id]] + providers = [AsyncHTTPProvider(str(url)) for url in rpc_endpoints[chain_id]] logger = logger or get_logger() From 82f99193d2a652d7071a02ae00dea2baf13366b8 Mon Sep 17 00:00:00 2001 From: zarathustra Date: Sun, 2 Nov 2025 17:21:54 +0100 Subject: [PATCH 16/22] feat: fully-typed client --- derive_client/_clients/__init__.py | 2 +- .../_clients/rest/async_http/account.py | 8 ++--- derive_client/_clients/rest/async_http/api.py | 4 +-- .../_clients/rest/async_http/client.py | 6 ++-- .../_clients/rest/async_http/markets.py | 2 +- derive_client/_clients/rest/async_http/mmp.py | 2 +- .../_clients/rest/async_http/orders.py | 4 +-- .../_clients/rest/async_http/positions.py | 6 ++-- derive_client/_clients/rest/async_http/rfq.py | 4 +-- .../_clients/rest/async_http/session.py | 3 -- .../_clients/rest/async_http/subaccount.py | 8 ++--- .../_clients/rest/async_http/transactions.py | 6 ++-- derive_client/_clients/rest/http/account.py | 8 ++--- derive_client/_clients/rest/http/api.py | 4 +-- derive_client/_clients/rest/http/client.py | 6 ++-- derive_client/_clients/rest/http/markets.py | 2 +- derive_client/_clients/rest/http/mmp.py | 2 +- derive_client/_clients/rest/http/orders.py | 4 +-- derive_client/_clients/rest/http/positions.py | 6 ++-- derive_client/_clients/rest/http/rfq.py | 4 +-- .../_clients/rest/http/subaccount.py | 8 ++--- .../_clients/rest/http/transactions.py | 6 ++-- derive_client/_clients/utils.py | 33 +++++++++---------- 23 files changed, 67 insertions(+), 71 deletions(-) diff --git a/derive_client/_clients/__init__.py b/derive_client/_clients/__init__.py index 3fb8d49d..a2794cde 100644 --- a/derive_client/_clients/__init__.py +++ b/derive_client/_clients/__init__.py @@ -1,7 +1,7 @@ """Clients module""" -from .rest.http.client import HTTPClient from .rest.async_http.client import AsyncHTTPClient +from .rest.http.client import HTTPClient __all__ = [ "HTTPClient", diff --git a/derive_client/_clients/rest/async_http/account.py b/derive_client/_clients/rest/async_http/account.py index d65057c7..ae86ab46 100644 --- a/derive_client/_clients/rest/async_http/account.py +++ b/derive_client/_clients/rest/async_http/account.py @@ -10,8 +10,9 @@ from derive_client._clients.rest.async_http.api import AsyncPrivateAPI, AsyncPublicAPI from derive_client._clients.utils import AuthContext -from derive_client.constants import CURRENCY_DECIMALS, Currency, EnvConfig -from derive_client.data.generated.models import ( +from derive_client.config import CURRENCY_DECIMALS, Currency, EnvConfig +from derive_client.data_types import ChecksumAddress +from derive_client.data_types.generated_models import ( MarginType, PrivateCreateSubaccountParamsSchema, PrivateCreateSubaccountResultSchema, @@ -32,7 +33,6 @@ PublicRegisterSessionKeyResultSchema, Scope, ) -from derive_client.data_types import Address class LightAccount: @@ -128,7 +128,7 @@ def state(self) -> PrivateGetAccountResultSchema: return self._state @property - def address(self) -> Address: + def address(self) -> ChecksumAddress: """LightAccount wallet address.""" return self._auth.wallet diff --git a/derive_client/_clients/rest/async_http/api.py b/derive_client/_clients/rest/async_http/api.py index e9802dac..acca2a9e 100644 --- a/derive_client/_clients/rest/async_http/api.py +++ b/derive_client/_clients/rest/async_http/api.py @@ -3,8 +3,8 @@ from derive_client._clients.rest.async_http.session import AsyncHTTPSession from derive_client._clients.rest.endpoints import PrivateEndpoints, PublicEndpoints from derive_client._clients.utils import AuthContext, encode_json_exclude_none, try_cast_response -from derive_client.constants import PUBLIC_HEADERS, EnvConfig -from derive_client.data.generated.models import ( +from derive_client.config import PUBLIC_HEADERS, EnvConfig +from derive_client.data_types.generated_models import ( PrivateCancelAllParamsSchema, PrivateCancelAllResponseSchema, PrivateCancelBatchQuotesParamsSchema, diff --git a/derive_client/_clients/rest/async_http/client.py b/derive_client/_clients/rest/async_http/client.py index 50c3e258..a2c198ba 100644 --- a/derive_client/_clients/rest/async_http/client.py +++ b/derive_client/_clients/rest/async_http/client.py @@ -20,8 +20,8 @@ from derive_client._clients.rest.async_http.subaccount import Subaccount from derive_client._clients.rest.async_http.transactions import TransactionOperations from derive_client._clients.utils import AuthContext -from derive_client.constants import CONFIGS -from derive_client.data_types import Address, Environment +from derive_client.config import CONFIGS +from derive_client.data_types import ChecksumAddress, Environment from derive_client.exceptions import BridgePrimarySignerRequiredError, NotConnectedError from derive_client.utils.logger import get_logger @@ -33,7 +33,7 @@ class AsyncHTTPClient: def __init__( self, *, - wallet: Address, + wallet: ChecksumAddress, session_key: str, subaccount_id: int, env: Environment, diff --git a/derive_client/_clients/rest/async_http/markets.py b/derive_client/_clients/rest/async_http/markets.py index 3bec7f8d..b2008933 100644 --- a/derive_client/_clients/rest/async_http/markets.py +++ b/derive_client/_clients/rest/async_http/markets.py @@ -7,7 +7,7 @@ from derive_client._clients.rest.async_http.api import AsyncPublicAPI from derive_client._clients.utils import async_fetch_all_pages_of_instrument_type, infer_instrument_type -from derive_client.data.generated.models import ( +from derive_client.data_types.generated_models import ( CurrencyDetailedResponseSchema, InstrumentPublicResponseSchema, InstrumentType, diff --git a/derive_client/_clients/rest/async_http/mmp.py b/derive_client/_clients/rest/async_http/mmp.py index 8ac20021..4cdeba0f 100644 --- a/derive_client/_clients/rest/async_http/mmp.py +++ b/derive_client/_clients/rest/async_http/mmp.py @@ -5,7 +5,7 @@ from decimal import Decimal from typing import TYPE_CHECKING, Optional -from derive_client.data.generated.models import ( +from derive_client.data_types.generated_models import ( MMPConfigResultSchema, PrivateGetMmpConfigParamsSchema, PrivateResetMmpParamsSchema, diff --git a/derive_client/_clients/rest/async_http/orders.py b/derive_client/_clients/rest/async_http/orders.py index d826fa5a..e64b1772 100644 --- a/derive_client/_clients/rest/async_http/orders.py +++ b/derive_client/_clients/rest/async_http/orders.py @@ -7,8 +7,8 @@ from derive_action_signing import TradeModuleData -from derive_client.constants import INT64_MAX -from derive_client.data.generated.models import ( +from derive_client.config import INT64_MAX +from derive_client.data_types.generated_models import ( Direction, OrderStatus, OrderType, diff --git a/derive_client/_clients/rest/async_http/positions.py b/derive_client/_clients/rest/async_http/positions.py index f323295c..6deae215 100644 --- a/derive_client/_clients/rest/async_http/positions.py +++ b/derive_client/_clients/rest/async_http/positions.py @@ -3,7 +3,7 @@ from __future__ import annotations from decimal import Decimal -from typing import List, TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, List, Optional from derive_action_signing import ( MakerTransferPositionModuleData, @@ -14,7 +14,8 @@ ) from derive_client._clients.utils import sort_by_instrument_name -from derive_client.data.generated.models import ( +from derive_client.data_types import PositionTransfer +from derive_client.data_types.generated_models import ( Direction, LegPricedSchema, PrivateGetPositionsParamsSchema, @@ -26,7 +27,6 @@ SignedQuoteParamsSchema, TradeModuleParamsSchema, ) -from derive_client.data_types import PositionTransfer if TYPE_CHECKING: from .subaccount import Subaccount diff --git a/derive_client/_clients/rest/async_http/rfq.py b/derive_client/_clients/rest/async_http/rfq.py index 4b4bc17d..daee9bec 100644 --- a/derive_client/_clients/rest/async_http/rfq.py +++ b/derive_client/_clients/rest/async_http/rfq.py @@ -12,8 +12,8 @@ ) from derive_client._clients.utils import sort_by_instrument_name -from derive_client.constants import UINT64_MAX -from derive_client.data.generated.models import ( +from derive_client.config import UINT64_MAX +from derive_client.data_types.generated_models import ( Direction, LegPricedSchema, LegUnpricedSchema, diff --git a/derive_client/_clients/rest/async_http/session.py b/derive_client/_clients/rest/async_http/session.py index 9c17a854..ade9ebef 100644 --- a/derive_client/_clients/rest/async_http/session.py +++ b/derive_client/_clients/rest/async_http/session.py @@ -5,8 +5,6 @@ import aiohttp -from derive_client.constants import PUBLIC_HEADERS - # Context-local timeout (task-scoped) used to temporarily override session timeout. _request_timeout_override: contextvars.ContextVar[float | None] = contextvars.ContextVar( '_request_timeout_override', default=None @@ -73,7 +71,6 @@ async def _send_request( ) -> bytes: session = await self.open() - headers = headers or PUBLIC_HEADERS total = _request_timeout_override.get() or self._request_timeout timeout = aiohttp.ClientTimeout(total=total) diff --git a/derive_client/_clients/rest/async_http/subaccount.py b/derive_client/_clients/rest/async_http/subaccount.py index 426cd1cc..26993447 100644 --- a/derive_client/_clients/rest/async_http/subaccount.py +++ b/derive_client/_clients/rest/async_http/subaccount.py @@ -16,13 +16,13 @@ from derive_client._clients.rest.async_http.rfq import RFQOperations from derive_client._clients.rest.async_http.transactions import TransactionOperations from derive_client._clients.utils import AuthContext -from derive_client.constants import EnvConfig -from derive_client.data.generated.models import ( +from derive_client.config import EnvConfig +from derive_client.data_types import ChecksumAddress +from derive_client.data_types.generated_models import ( MarginType, PrivateGetSubaccountParamsSchema, PrivateGetSubaccountResultSchema, ) -from derive_client.data_types import Address @functools.total_ordering @@ -173,7 +173,7 @@ def mmp(self) -> MMPOperations: def sign_action( self, *, - module_address: Address | str, + module_address: ChecksumAddress, module_data: ModuleData, signature_expiry_sec: Optional[int] = None, nonce: Optional[int] | None = None, diff --git a/derive_client/_clients/rest/async_http/transactions.py b/derive_client/_clients/rest/async_http/transactions.py index 8901e7a9..a69d2422 100644 --- a/derive_client/_clients/rest/async_http/transactions.py +++ b/derive_client/_clients/rest/async_http/transactions.py @@ -7,8 +7,9 @@ from derive_action_signing import DepositModuleData, WithdrawModuleData -from derive_client.constants import CURRENCY_DECIMALS -from derive_client.data.generated.models import ( +from derive_client.config import CURRENCY_DECIMALS +from derive_client.data_types import Currency +from derive_client.data_types.generated_models import ( MarginType, PrivateDepositParamsSchema, PrivateDepositResultSchema, @@ -17,7 +18,6 @@ PublicGetTransactionParamsSchema, PublicGetTransactionResultSchema, ) -from derive_client.data_types import Currency if TYPE_CHECKING: from .subaccount import Subaccount diff --git a/derive_client/_clients/rest/http/account.py b/derive_client/_clients/rest/http/account.py index 1c6cda9b..e8e1ae59 100644 --- a/derive_client/_clients/rest/http/account.py +++ b/derive_client/_clients/rest/http/account.py @@ -10,8 +10,9 @@ from derive_client._clients.rest.http.api import PrivateAPI, PublicAPI from derive_client._clients.utils import AuthContext -from derive_client.constants import CURRENCY_DECIMALS, Currency, EnvConfig -from derive_client.data.generated.models import ( +from derive_client.config import CURRENCY_DECIMALS, Currency, EnvConfig +from derive_client.data_types import ChecksumAddress +from derive_client.data_types.generated_models import ( MarginType, PrivateCreateSubaccountParamsSchema, PrivateCreateSubaccountResultSchema, @@ -32,7 +33,6 @@ PublicRegisterSessionKeyResultSchema, Scope, ) -from derive_client.data_types import Address class LightAccount: @@ -128,7 +128,7 @@ def state(self) -> PrivateGetAccountResultSchema: return self._state @property - def address(self) -> Address: + def address(self) -> ChecksumAddress: """LightAccount wallet address.""" return self._auth.wallet diff --git a/derive_client/_clients/rest/http/api.py b/derive_client/_clients/rest/http/api.py index bd0bef68..3c329692 100644 --- a/derive_client/_clients/rest/http/api.py +++ b/derive_client/_clients/rest/http/api.py @@ -3,8 +3,8 @@ from derive_client._clients.rest.endpoints import PrivateEndpoints, PublicEndpoints from derive_client._clients.rest.http.session import HTTPSession from derive_client._clients.utils import AuthContext, encode_json_exclude_none, try_cast_response -from derive_client.constants import PUBLIC_HEADERS, EnvConfig -from derive_client.data.generated.models import ( +from derive_client.config import PUBLIC_HEADERS, EnvConfig +from derive_client.data_types.generated_models import ( PrivateCancelAllParamsSchema, PrivateCancelAllResponseSchema, PrivateCancelBatchQuotesParamsSchema, diff --git a/derive_client/_clients/rest/http/client.py b/derive_client/_clients/rest/http/client.py index 424dca23..df1c7ce5 100644 --- a/derive_client/_clients/rest/http/client.py +++ b/derive_client/_clients/rest/http/client.py @@ -19,8 +19,8 @@ from derive_client._clients.rest.http.subaccount import Subaccount from derive_client._clients.rest.http.transactions import TransactionOperations from derive_client._clients.utils import AuthContext -from derive_client.constants import CONFIGS -from derive_client.data_types import Address, Environment +from derive_client.config import CONFIGS +from derive_client.data_types import ChecksumAddress, Environment from derive_client.exceptions import BridgePrimarySignerRequiredError, NotConnectedError from derive_client.utils.logger import get_logger @@ -32,7 +32,7 @@ class HTTPClient: def __init__( self, *, - wallet: Address, + wallet: ChecksumAddress, session_key: str, subaccount_id: int, env: Environment, diff --git a/derive_client/_clients/rest/http/markets.py b/derive_client/_clients/rest/http/markets.py index ca574c57..79e8ed6a 100644 --- a/derive_client/_clients/rest/http/markets.py +++ b/derive_client/_clients/rest/http/markets.py @@ -7,7 +7,7 @@ from derive_client._clients.rest.http.api import PublicAPI from derive_client._clients.utils import fetch_all_pages_of_instrument_type, infer_instrument_type -from derive_client.data.generated.models import ( +from derive_client.data_types.generated_models import ( CurrencyDetailedResponseSchema, InstrumentPublicResponseSchema, InstrumentType, diff --git a/derive_client/_clients/rest/http/mmp.py b/derive_client/_clients/rest/http/mmp.py index 20663669..046f8da1 100644 --- a/derive_client/_clients/rest/http/mmp.py +++ b/derive_client/_clients/rest/http/mmp.py @@ -5,7 +5,7 @@ from decimal import Decimal from typing import TYPE_CHECKING, Optional -from derive_client.data.generated.models import ( +from derive_client.data_types.generated_models import ( MMPConfigResultSchema, PrivateGetMmpConfigParamsSchema, PrivateResetMmpParamsSchema, diff --git a/derive_client/_clients/rest/http/orders.py b/derive_client/_clients/rest/http/orders.py index 9927bf05..a416de31 100644 --- a/derive_client/_clients/rest/http/orders.py +++ b/derive_client/_clients/rest/http/orders.py @@ -7,8 +7,8 @@ from derive_action_signing import TradeModuleData -from derive_client.constants import INT64_MAX -from derive_client.data.generated.models import ( +from derive_client.config import INT64_MAX +from derive_client.data_types.generated_models import ( Direction, OrderStatus, OrderType, diff --git a/derive_client/_clients/rest/http/positions.py b/derive_client/_clients/rest/http/positions.py index 822a1fa1..600f2ebf 100644 --- a/derive_client/_clients/rest/http/positions.py +++ b/derive_client/_clients/rest/http/positions.py @@ -3,7 +3,7 @@ from __future__ import annotations from decimal import Decimal -from typing import List, TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, List, Optional from derive_action_signing import ( MakerTransferPositionModuleData, @@ -14,7 +14,8 @@ ) from derive_client._clients.utils import sort_by_instrument_name -from derive_client.data.generated.models import ( +from derive_client.data_types import PositionTransfer +from derive_client.data_types.generated_models import ( Direction, LegPricedSchema, PrivateGetPositionsParamsSchema, @@ -26,7 +27,6 @@ SignedQuoteParamsSchema, TradeModuleParamsSchema, ) -from derive_client.data_types import PositionTransfer if TYPE_CHECKING: from .subaccount import Subaccount diff --git a/derive_client/_clients/rest/http/rfq.py b/derive_client/_clients/rest/http/rfq.py index 1e6f835b..402120ae 100644 --- a/derive_client/_clients/rest/http/rfq.py +++ b/derive_client/_clients/rest/http/rfq.py @@ -12,8 +12,8 @@ ) from derive_client._clients.utils import sort_by_instrument_name -from derive_client.constants import UINT64_MAX -from derive_client.data.generated.models import ( +from derive_client.config import UINT64_MAX +from derive_client.data_types.generated_models import ( Direction, LegPricedSchema, LegUnpricedSchema, diff --git a/derive_client/_clients/rest/http/subaccount.py b/derive_client/_clients/rest/http/subaccount.py index afe5a9c8..eb2da9f6 100644 --- a/derive_client/_clients/rest/http/subaccount.py +++ b/derive_client/_clients/rest/http/subaccount.py @@ -16,13 +16,13 @@ from derive_client._clients.rest.http.rfq import RFQOperations from derive_client._clients.rest.http.transactions import TransactionOperations from derive_client._clients.utils import AuthContext -from derive_client.constants import EnvConfig -from derive_client.data.generated.models import ( +from derive_client.config import EnvConfig +from derive_client.data_types import ChecksumAddress +from derive_client.data_types.generated_models import ( MarginType, PrivateGetSubaccountParamsSchema, PrivateGetSubaccountResultSchema, ) -from derive_client.data_types import Address @functools.total_ordering @@ -173,7 +173,7 @@ def mmp(self) -> MMPOperations: def sign_action( self, *, - module_address: Address | str, + module_address: ChecksumAddress, module_data: ModuleData, signature_expiry_sec: Optional[int] = None, nonce: Optional[int] | None = None, diff --git a/derive_client/_clients/rest/http/transactions.py b/derive_client/_clients/rest/http/transactions.py index edf9d7b1..025c8ac3 100644 --- a/derive_client/_clients/rest/http/transactions.py +++ b/derive_client/_clients/rest/http/transactions.py @@ -7,8 +7,9 @@ from derive_action_signing import DepositModuleData, WithdrawModuleData -from derive_client.constants import CURRENCY_DECIMALS -from derive_client.data.generated.models import ( +from derive_client.config import CURRENCY_DECIMALS +from derive_client.data_types import Currency +from derive_client.data_types.generated_models import ( MarginType, PrivateDepositParamsSchema, PrivateDepositResultSchema, @@ -17,7 +18,6 @@ PublicGetTransactionParamsSchema, PublicGetTransactionResultSchema, ) -from derive_client.data_types import Currency if TYPE_CHECKING: from .subaccount import Subaccount diff --git a/derive_client/_clients/utils.py b/derive_client/_clients/utils.py index 1888d68e..0e2856b7 100644 --- a/derive_client/_clients/utils.py +++ b/derive_client/_clients/utils.py @@ -7,22 +7,21 @@ from typing import TYPE_CHECKING, Iterable, Optional, TypeVar import msgspec -from derive_action_signing import ModuleData, SignedAction -from derive_action_signing.utils import sign_rest_auth_header -from eth_account import Account +from derive_action_signing import ModuleData, SignedAction, sign_rest_auth_header +from eth_account.signers.local import LocalAccount +from hexbytes import HexBytes from pydantic import BaseModel from web3 import AsyncWeb3, Web3 -from derive_client.constants import EnvConfig -from derive_client.data.generated.models import ( +from derive_client.config import EnvConfig +from derive_client.data_types import ChecksumAddress, PositionTransfer +from derive_client.data_types.generated_models import ( InstrumentPublicResponseSchema, InstrumentType, LegPricedSchema, LegUnpricedSchema, - PositionTransfer, RPCErrorFormatSchema, ) -from derive_client.data_types import Address if TYPE_CHECKING: from derive_client._clients.rest.async_http.markets import MarketOperations as AsyncMarketOperations @@ -56,26 +55,26 @@ def get_default_signature_expiry_sec() -> int: @dataclass class AuthContext: - wallet: Address + wallet: ChecksumAddress w3: Web3 | AsyncWeb3 - account: Account + account: LocalAccount config: EnvConfig @property - def signer(self) -> Address: - return self.account.address + def signer(self) -> ChecksumAddress: + return ChecksumAddress(self.account.address) @property def signed_headers(self): return sign_rest_auth_header( - web3_client=self.w3, + web3_client=self.w3, # type: ignore smart_contract_wallet=self.wallet, - session_key_or_wallet_private_key=self.account.key, + session_key_or_wallet_private_key=HexBytes(self.account.key).to_0x_hex(), ) def sign_action( self, - module_address: Address | str, + module_address: ChecksumAddress, module_data: ModuleData, subaccount_id: int, signature_expiry_sec: Optional[int] = None, @@ -97,7 +96,7 @@ def sign_action( DOMAIN_SEPARATOR=self.config.DOMAIN_SEPARATOR, ACTION_TYPEHASH=self.config.ACTION_TYPEHASH, ) - action.sign(self.account.key) + action.sign(HexBytes(self.account.key).to_0x_hex()) return action @@ -192,7 +191,7 @@ def fetch_all_pages_of_instrument_type( page_size=page_size, ) instruments.extend(result.instruments) - if page >= result.pagination.num_pages: + if not result.pagination or page >= result.pagination.num_pages: break page += 1 @@ -218,7 +217,7 @@ async def async_fetch_all_pages_of_instrument_type( page_size=page_size, ) instruments.extend(result.instruments) - if page >= result.pagination.num_pages: + if not result.pagination or page >= result.pagination.num_pages: break page += 1 From b1c39e7b658297863a7941e25fd8b26f1e74ef1f Mon Sep 17 00:00:00 2001 From: zarathustra Date: Sun, 2 Nov 2025 17:24:25 +0100 Subject: [PATCH 17/22] feat: fully-typed bridge client --- derive_client/_bridge/_derive_bridge.py | 182 +++++++++++++--------- derive_client/_bridge/_standard_bridge.py | 58 +++---- derive_client/_bridge/async_client.py | 41 ++--- derive_client/_bridge/client.py | 41 ++--- derive_client/_bridge/w3.py | 173 ++++++++++++-------- 5 files changed, 289 insertions(+), 206 deletions(-) diff --git a/derive_client/_bridge/_derive_bridge.py b/derive_client/_bridge/_derive_bridge.py index 2c45a5f8..f897ba03 100644 --- a/derive_client/_bridge/_derive_bridge.py +++ b/derive_client/_bridge/_derive_bridge.py @@ -6,17 +6,15 @@ import json from decimal import Decimal from logging import Logger +from typing import TypeGuard, cast -from eth_account import Account +from eth_account.signers.local import LocalAccount +from eth_typing import HexStr from returns.future import future_safe -from returns.io import IOResult from web3 import AsyncWeb3 -from web3.contract import AsyncContract -from web3.contract.async_contract import AsyncContractFunction -from web3.datastructures import AttributeDict -from web3.types import HexBytes, LogReceipt, TxReceipt +from web3.contract.async_contract import AsyncContract, AsyncContractEvent, AsyncContractFunction -from derive_client.constants import ( +from derive_client.config import ( ARBITRUM_DEPOSIT_WRAPPER, BASE_DEPOSIT_WRAPPER, CONTROLLER_ABI_PATH, @@ -40,23 +38,26 @@ TARGET_SPEED, WITHDRAW_WRAPPER_V2, WITHDRAW_WRAPPER_V2_ABI_PATH, + DeriveTokenAddress, + LayerZeroChainIDv2, + SocketAddress, ) from derive_client.data_types import ( - Address, BridgeContext, + BridgeDirection, BridgeTxDetails, BridgeTxResult, BridgeType, ChainID, + ChecksumAddress, Currency, - DeriveTokenAddresses, - Direction, - LayerZeroChainIDv2, MintableTokenData, NonMintableTokenData, PreparedBridgeTx, - SocketAddress, + TxHash, TxResult, + TypedLogReceipt, + TypedTxReceipt, ) from derive_client.exceptions import ( BridgeEventParseError, @@ -93,21 +94,30 @@ def _load_controller_contract(w3: AsyncWeb3, token_data: MintableTokenData) -> A def _load_deposit_contract(w3: AsyncWeb3, token_data: MintableTokenData) -> AsyncContract: + assert token_data.LyraTSAShareHandlerDepositHook is not None, "Expected LyraTSAShareHandlerDepositHook to exist" address = token_data.LyraTSAShareHandlerDepositHook abi = json.loads(DEPOSIT_HOOK_ABI_PATH.read_text()) return get_contract(w3=w3, address=address, abi=abi) -def _load_light_account(w3: AsyncWeb3, wallet: Address) -> AsyncContract: +def _load_light_account(w3: AsyncWeb3, wallet: ChecksumAddress) -> AsyncContract: abi = json.loads(LIGHT_ACCOUNT_ABI_PATH.read_text()) return get_contract(w3=w3, address=wallet, abi=abi) +def is_mintable(data: MintableTokenData | NonMintableTokenData) -> TypeGuard[MintableTokenData]: + return isinstance(data, MintableTokenData) + + +def is_non_mintable(data: MintableTokenData | NonMintableTokenData) -> TypeGuard[NonMintableTokenData]: + return isinstance(data, NonMintableTokenData) + + def _get_min_fees( bridge_contract: AsyncContract, - connector: Address, + connector: ChecksumAddress, token_data: NonMintableTokenData | MintableTokenData, -) -> int: +) -> AsyncContractFunction: params = { "connector_": connector, "msgGasLimit_": MSG_GAS_LIMIT, @@ -121,18 +131,18 @@ def _get_min_fees( class DeriveBridge: """Bridge ERC-20 tokens and Derive's native token (DRV) to and from Derive.""" - def __init__(self, account: Account, wallet: Address, logger: Logger): + def __init__(self, account: LocalAccount, wallet: ChecksumAddress, logger: Logger): """ Initialize Derive bridge. Args: - account: Account object containing the private key of the owner of the smart contract funding account + account: LocalAccount object containing the private key of the owner of the smart contract funding account wallet: Address of the smart contract funding account logger: Logger instance for logging """ self.account = account - self.owner = account.address + self.owner = ChecksumAddress(account.address) # type: ignore[attr-defined] self.wallet = wallet self.derive_addresses = get_prod_derive_addresses() self.w3s = get_w3_connections(logger=logger) @@ -143,9 +153,9 @@ def derive_w3(self) -> AsyncWeb3: return self.w3s[ChainID.DERIVE] @property - def private_key(self) -> HexBytes: + def private_key(self) -> str: """Private key of the owner (EOA) of the smart contract funding account.""" - return self.account._private_key + return self.account.key.to_0x_hex() # type: ignore[attr-defined] @functools.cached_property def light_account(self): @@ -177,11 +187,11 @@ def withdraw_wrapper(self) -> AsyncContract: @functools.lru_cache def _make_bridge_context( self, - direction: Direction, + direction: BridgeDirection, currency: Currency, remote_chain_id: ChainID, ) -> BridgeContext: - is_deposit = direction == Direction.DEPOSIT + is_deposit = direction == BridgeDirection.DEPOSIT if is_deposit: src_w3, tgt_w3 = self.w3s[remote_chain_id], self.derive_w3 @@ -191,26 +201,39 @@ def _make_bridge_context( src_chain, tgt_chain = ChainID.DERIVE, remote_chain_id if currency is Currency.DRV: - src_addr = DeriveTokenAddresses[src_chain.name].value - tgt_addr = DeriveTokenAddresses[tgt_chain.name].value + src_addr = DeriveTokenAddress[src_chain.name].value + tgt_addr = DeriveTokenAddress[tgt_chain.name].value derive_abi = json.loads(DERIVE_L2_ABI_PATH.read_text()) remote_abi_path = DERIVE_ABI_PATH if remote_chain_id == ChainID.ETH else DERIVE_L2_ABI_PATH remote_abi = json.loads(remote_abi_path.read_text()) src_abi, tgt_abi = (remote_abi, derive_abi) if is_deposit else (derive_abi, remote_abi) - src = get_contract(src_w3, src_addr, abi=src_abi) - tgt = get_contract(tgt_w3, tgt_addr, abi=tgt_abi) + src = get_contract(w3=src_w3, address=src_addr, abi=src_abi) + tgt = get_contract(w3=tgt_w3, address=tgt_addr, abi=tgt_abi) src_event, tgt_event = src.events.OFTSent(), tgt.events.OFTReceived() - context = BridgeContext(currency, src_w3, tgt_w3, src, src_event, tgt_event, src_chain, tgt_chain) - return context + + return BridgeContext( + currency=currency, + source_w3=src_w3, + target_w3=tgt_w3, + source_token=src, + source_event=cast(AsyncContractEvent, src_event), + target_event=cast(AsyncContractEvent, tgt_event), + source_chain=src_chain, + target_chain=tgt_chain, + ) erc20_abi = json.loads(ERC20_ABI_PATH.read_text()) socket_abi = json.loads(SOCKET_ABI_PATH.read_text()) + token_data = self.derive_addresses.chains[src_chain][currency] + if is_deposit: - token_data: NonMintableTokenData = self.derive_addresses.chains[src_chain][currency] + if not isinstance(token_data, NonMintableTokenData): + raise TypeError("expected NonMintableTokenData for deposit") token_contract = get_contract(src_w3, token_data.NonMintableToken, abi=erc20_abi) else: - token_data: MintableTokenData = self.derive_addresses.chains[src_chain][currency] + if not isinstance(token_data, MintableTokenData): + raise TypeError("expected MintableTokenData for withdrawal") token_contract = get_contract(src_w3, token_data.MintableToken, abi=erc20_abi) src_addr = SocketAddress[src_chain.name].value @@ -218,12 +241,21 @@ def _make_bridge_context( src_socket = get_contract(src_w3, address=src_addr, abi=socket_abi) tgt_socket = get_contract(tgt_w3, address=tgt_addr, abi=socket_abi) src_event, tgt_event = src_socket.events.MessageOutbound(), tgt_socket.events.ExecutionSuccess() - context = BridgeContext(currency, src_w3, tgt_w3, token_contract, src_event, tgt_event, src_chain, tgt_chain) - return context + + return BridgeContext( + currency=currency, + source_w3=src_w3, + target_w3=tgt_w3, + source_token=token_contract, + source_event=cast(AsyncContractEvent, src_event), + target_event=cast(AsyncContractEvent, tgt_event), + source_chain=src_chain, + target_chain=tgt_chain, + ) def _get_context(self, state: PreparedBridgeTx | BridgeTxResult) -> BridgeContext: - direction = Direction.WITHDRAW if state.source_chain == ChainID.DERIVE else Direction.DEPOSIT - remote_chain_id = state.target_chain if direction == Direction.WITHDRAW else state.source_chain + direction = BridgeDirection.WITHDRAW if state.source_chain == ChainID.DERIVE else BridgeDirection.DEPOSIT + remote_chain_id = state.target_chain if direction == BridgeDirection.WITHDRAW else state.source_chain context = self._make_bridge_context( direction=direction, currency=state.currency, @@ -235,7 +267,7 @@ def _get_context(self, state: PreparedBridgeTx | BridgeTxResult) -> BridgeContex def _resolve_socket_route( self, context: BridgeContext, - ) -> tuple[MintableTokenData | NonMintableTokenData, Address]: + ) -> tuple[MintableTokenData | NonMintableTokenData, ChecksumAddress]: currency = context.currency src_chain, tgt_chain = context.source_chain, context.target_chain @@ -253,7 +285,7 @@ def _resolve_socket_route( msg = f"Source chain {src_chain.name} not found in {tgt_chain.name} connectors." raise BridgeRouteError(msg) - return src_token_data, src_token_data.connectors[tgt_chain][TARGET_SPEED] + return src_token_data, ChecksumAddress(src_token_data.connectors[tgt_chain][TARGET_SPEED]) async def _prepare_tx( self, @@ -276,8 +308,8 @@ async def _prepare_tx( tx_details = BridgeTxDetails( contract=func.address, - method=func.fn_name, - kwargs=func.kwargs, + fn_name=func.fn_name, + fn_kwargs=func.kwargs, tx=tx, signed_tx=signed_tx, ) @@ -302,19 +334,19 @@ async def prepare_deposit( amount: Decimal, currency: Currency, chain_id: ChainID, - ) -> IOResult[PreparedBridgeTx, Exception]: + ) -> PreparedBridgeTx: if currency is Currency.ETH: raise NotImplementedError("ETH deposits are not implemented.") - amount: int = to_base_units(decimal_amount=amount, currency=currency) - direction = Direction.DEPOSIT + amount_base_units: int = to_base_units(decimal_amount=amount, currency=currency) + direction = BridgeDirection.DEPOSIT if currency == Currency.DRV: context = self._make_bridge_context(direction, currency=currency, remote_chain_id=chain_id) - prepared_tx = await self._prepare_layerzero_deposit(amount=amount, context=context) + prepared_tx = await self._prepare_layerzero_deposit(amount=amount_base_units, context=context) else: context = self._make_bridge_context(direction, currency=currency, remote_chain_id=chain_id) - prepared_tx = await self._prepare_socket_deposit(amount=amount, context=context) + prepared_tx = await self._prepare_socket_deposit(amount=amount_base_units, context=context) return prepared_tx @@ -324,30 +356,30 @@ async def prepare_withdrawal( amount: Decimal, currency: Currency, chain_id: ChainID, - ) -> IOResult[PreparedBridgeTx, Exception]: + ) -> PreparedBridgeTx: if currency is Currency.ETH: raise NotImplementedError("ETH withdrawals are not implemented.") - amount: int = to_base_units(decimal_amount=amount, currency=currency) - direction = Direction.WITHDRAW + amount_base_units: int = to_base_units(decimal_amount=amount, currency=currency) + direction = BridgeDirection.WITHDRAW if currency == Currency.DRV: context = self._make_bridge_context(direction, currency=currency, remote_chain_id=chain_id) - prepared_tx = await self._prepare_layerzero_withdrawal(amount=amount, context=context) + prepared_tx = await self._prepare_layerzero_withdrawal(amount=amount_base_units, context=context) else: context = self._make_bridge_context(direction, currency=currency, remote_chain_id=chain_id) - prepared_tx = await self._prepare_socket_withdrawal(amount=amount, context=context) + prepared_tx = await self._prepare_socket_withdrawal(amount=amount_base_units, context=context) return prepared_tx @future_safe - async def submit_bridge_tx(self, prepared_tx: PreparedBridgeTx) -> IOResult[BridgeTxResult, Exception]: + async def submit_bridge_tx(self, prepared_tx: PreparedBridgeTx) -> BridgeTxResult: tx_result = await self._send_bridge_tx(prepared_tx=prepared_tx) return tx_result @future_safe - async def poll_bridge_progress(self, tx_result: BridgeTxResult) -> IOResult[BridgeTxResult, Exception]: + async def poll_bridge_progress(self, tx_result: BridgeTxResult) -> BridgeTxResult: try: tx_result.source_tx.tx_receipt = await self._confirm_source_tx(tx_result=tx_result) tx_result.target_tx = TxResult(tx_hash=await self._wait_for_target_event(tx_result=tx_result)) @@ -359,14 +391,17 @@ async def poll_bridge_progress(self, tx_result: BridgeTxResult) -> IOResult[Brid async def _prepare_socket_deposit(self, amount: int, context: BridgeContext) -> PreparedBridgeTx: token_data, _connector = self._resolve_socket_route(context=context) + assert is_non_mintable(token_data), "Expected NonMintableTokenData" spender = token_data.Vault if token_data.isNewBridge else self.get_deposit_helper(context.source_chain).address + assert is_non_mintable(token_data), "Expected NonMintableTokenData" + await ensure_token_balance(context.source_token, self.owner, amount=amount) await ensure_token_allowance( w3=context.source_w3, token_contract=context.source_token, owner=self.owner, - spender=spender, + spender=ChecksumAddress(spender), amount=amount, private_key=self.private_key, logger=self.logger, @@ -384,6 +419,7 @@ async def _prepare_socket_deposit(self, amount: int, context: BridgeContext) -> async def _prepare_socket_withdrawal(self, amount: int, context: BridgeContext) -> PreparedBridgeTx: token_data, connector = self._resolve_socket_route(context=context) + assert is_mintable(token_data), "Expected MintableTokenData" # Get estimated fee in token for a withdrawal fee_in_token = await self.withdraw_wrapper.functions.getFeeInToken( @@ -430,14 +466,14 @@ async def _prepare_layerzero_deposit(self, amount: int, context: BridgeContext) w3=context.source_w3, token_contract=context.source_token, owner=self.owner, - spender=context.source_token.address, + spender=ChecksumAddress(context.source_token.address), amount=amount, private_key=self.private_key, logger=self.logger, ) # build the send tx - receiver_bytes32 = AsyncWeb3.to_bytes(hexstr=self.wallet).rjust(32, b"\x00") + receiver_bytes32 = AsyncWeb3.to_bytes(hexstr=cast(HexStr, self.wallet)).rjust(32, b"\x00") kwargs = { "dstEid": LayerZeroChainIDv2.DERIVE.value, @@ -520,7 +556,7 @@ async def _send_bridge_tx(self, prepared_tx: PreparedBridgeTx) -> BridgeTxResult return tx_result - async def _confirm_source_tx(self, tx_result: BridgeTxResult) -> TxReceipt: + async def _confirm_source_tx(self, tx_result: BridgeTxResult) -> TypedTxReceipt: context = self._get_context(tx_result) msg = "⏳ Checking source chain [%s] tx receipt for %s" self.logger.info(msg, tx_result.source_chain.name, tx_result.source_tx.tx_hash) @@ -532,7 +568,7 @@ async def _confirm_source_tx(self, tx_result: BridgeTxResult) -> TxReceipt: return tx_receipt - async def _wait_for_target_event(self, tx_result: BridgeTxResult) -> HexBytes: + async def _wait_for_target_event(self, tx_result: BridgeTxResult) -> TxHash: bridge_event_fetchers = { BridgeType.SOCKET: self._fetch_socket_event_log, BridgeType.LAYERZERO: self._fetch_lz_event_log, @@ -542,12 +578,14 @@ async def _wait_for_target_event(self, tx_result: BridgeTxResult) -> HexBytes: context = self._get_context(tx_result) event_log = await fetch_event(tx_result, context) - tx_hash = event_log["transactionHash"] + tx_hash = event_log.transactionHash self.logger.info(f"Target event tx_hash found: {tx_hash.to_0x_hex()}") - return tx_hash + return TxHash(tx_hash) + + async def _confirm_target_tx(self, tx_result: BridgeTxResult) -> TypedTxReceipt: + assert tx_result.target_tx is not None, "Expected tx_result.target_tx to exist" - async def _confirm_target_tx(self, tx_result: BridgeTxResult) -> TxReceipt: context = self._get_context(tx_result) msg = "⏳ Checking target chain [%s] tx receipt for %s" self.logger.info(msg, tx_result.target_chain.name, tx_result.target_tx.tx_hash) @@ -559,7 +597,9 @@ async def _confirm_target_tx(self, tx_result: BridgeTxResult) -> TxReceipt: return tx_receipt - async def _fetch_lz_event_log(self, tx_result: BridgeTxResult, context: BridgeContext) -> LogReceipt: + async def _fetch_lz_event_log(self, tx_result: BridgeTxResult, context: BridgeContext) -> TypedLogReceipt: + assert tx_result.source_tx.tx_receipt, "Expected source_tx.receipt to exist" + try: source_event = context.source_event.process_log(tx_result.source_tx.tx_receipt.logs[-1]) guid = source_event["args"]["guid"] @@ -585,7 +625,9 @@ async def _fetch_lz_event_log(self, tx_result: BridgeTxResult, context: BridgeCo logger=self.logger, ) - async def _fetch_socket_event_log(self, tx_result: BridgeTxResult, context: BridgeContext) -> LogReceipt: + async def _fetch_socket_event_log(self, tx_result: BridgeTxResult, context: BridgeContext) -> TypedLogReceipt: + assert tx_result.source_tx.tx_receipt, "Expected source_tx.receipt to exist" + try: source_event = context.source_event.process_log(tx_result.source_tx.tx_receipt.logs[-2]) message_id = source_event["args"]["msgId"] @@ -598,13 +640,12 @@ async def _fetch_socket_event_log(self, tx_result: BridgeTxResult, context: Brid fromBlock=tx_result.target_from_block, abi=context.target_event.abi ) - def matching_message_id(log: AttributeDict) -> bool: - decoded = context.target_event.process_log(log) + def matching_message_id(log: TypedLogReceipt) -> bool: + decoded = context.target_event.process_log(log.to_w3()) return decoded.get("args", {}).get("msgId") == message_id - self.logger.info( - f"🔍 Listening for ExecutionSuccess on [{tx_result.target_chain.name}] at {context.target_event.address}" - ) + msg = f"🔍 Listening for ExecutionSuccess on [{tx_result.target_chain.name}] at {context.target_event.address}" + self.logger.info(msg) return await wait_for_bridge_event( w3=context.target_w3, @@ -618,7 +659,7 @@ def _prepare_new_style_deposit( token_data: NonMintableTokenData, amount: int, context: BridgeContext, - ) -> tuple[AsyncContractFunction, int]: + ) -> tuple[AsyncContractFunction, AsyncContractFunction]: vault_contract = _load_vault_contract(w3=self.w3s[context.source_chain], token_data=token_data) connector = token_data.connectors[ChainID.DERIVE][TARGET_SPEED] fees_func = _get_min_fees(bridge_contract=vault_contract, connector=connector, token_data=token_data) @@ -638,7 +679,7 @@ def _prepare_old_style_deposit( token_data: NonMintableTokenData, amount: int, context: BridgeContext, - ) -> tuple[AsyncContractFunction, int]: + ) -> tuple[AsyncContractFunction, AsyncContractFunction]: vault_contract = _load_vault_contract(w3=self.w3s[context.source_chain], token_data=token_data) connector = token_data.connectors[ChainID.DERIVE][TARGET_SPEED] fees_func = _get_min_fees(bridge_contract=vault_contract, connector=connector, token_data=token_data) @@ -653,7 +694,7 @@ def _prepare_old_style_deposit( return func, fees_func - async def _check_bridge_funds(self, token_data, connector: Address, amount: int) -> None: + async def _check_bridge_funds(self, token_data, connector: ChecksumAddress, amount: int) -> None: controller = _load_controller_contract(w3=self.derive_w3, token_data=token_data) if token_data.isNewBridge: deposit_hook = await controller.functions.hook__().call() @@ -669,6 +710,5 @@ async def _check_bridge_funds(self, token_data, connector: Address, amount: int) locked = await controller.functions.poolLockedAmounts(pool_id).call() if amount > locked: - raise RuntimeError( - f"Insufficient funds locked in pool: has {locked}, want {amount} ({(locked / amount * 100):.2f}%)" - ) + msg = f"Insufficient funds locked in pool: has {locked}, want {amount} ({(locked / amount * 100):.2f}%)" + raise RuntimeError(msg) diff --git a/derive_client/_bridge/_standard_bridge.py b/derive_client/_bridge/_standard_bridge.py index 4d82fa91..d0b8ac07 100644 --- a/derive_client/_bridge/_standard_bridge.py +++ b/derive_client/_bridge/_standard_bridge.py @@ -4,16 +4,15 @@ import json from decimal import Decimal from logging import Logger +from typing import cast -from eth_account import Account -from eth_utils import keccak +from eth_account.signers.local import LocalAccount +from eth_utils.crypto import keccak from returns.future import future_safe -from returns.io import IOResult from web3 import AsyncWeb3 -from web3.contract import AsyncContract -from web3.types import HexBytes, LogReceipt, TxReceipt +from web3.contract.async_contract import AsyncContract, AsyncContractEvent -from derive_client.constants import ( +from derive_client.config import ( L1_CHUG_SPLASH_PROXY, L1_CROSS_DOMAIN_MESSENGER_ABI_PATH, L1_STANDARD_BRIDGE_ABI_PATH, @@ -25,14 +24,17 @@ RESOLVED_DELEGATE_PROXY, ) from derive_client.data_types import ( - Address, BridgeTxDetails, BridgeTxResult, BridgeType, ChainID, + ChecksumAddress, Currency, PreparedBridgeTx, + TxHash, TxResult, + TypedLogReceipt, + TypedTxReceipt, ) from derive_client.exceptions import BridgeEventParseError, PartialBridgeResult, StandardBridgeRelayFailed from derive_client.utils.w3 import to_base_units @@ -81,7 +83,7 @@ def _load_l2_cross_domain_messenger_proxy(w3: AsyncWeb3) -> AsyncContract: class StandardBridge: """Bridge tokens using Optimism's native standard bridge.""" - def __init__(self, account: Account, logger: Logger): + def __init__(self, account: LocalAccount, logger: Logger): """ Initialize Standard bridge. @@ -102,10 +104,10 @@ def __init__(self, account: Account, logger: Logger): async def prepare_eth_tx( self, amount: Decimal, - to: Address, + to: ChecksumAddress, source_chain: ChainID, target_chain: ChainID, - ) -> IOResult[PreparedBridgeTx, Exception]: + ) -> PreparedBridgeTx: currency = Currency.ETH if source_chain is not ChainID.ETH or target_chain is not ChainID.DERIVE or to != self.account.address: @@ -124,16 +126,16 @@ async def prepare_eth_tx( @property def private_key(self) -> str: """Private key of the owner (EOA).""" - return self.account._private_key + return self.account.key.to_0x_hex() # type: ignore[attr-defined] @future_safe - async def submit_bridge_tx(self, prepared_tx: PreparedBridgeTx) -> IOResult[BridgeTxResult, Exception]: + async def submit_bridge_tx(self, prepared_tx: PreparedBridgeTx) -> BridgeTxResult: tx_result = await self._send_bridge_tx(prepared_tx=prepared_tx) return tx_result @future_safe - async def poll_bridge_progress(self, tx_result: BridgeTxResult) -> IOResult[BridgeTxResult, Exception]: + async def poll_bridge_progress(self, tx_result: BridgeTxResult) -> BridgeTxResult: try: tx_result.source_tx.tx_receipt = await self._confirm_source_tx(tx_result=tx_result) tx_result.target_tx = TxResult(tx_hash=await self._wait_for_target_event(tx_result=tx_result)) @@ -146,7 +148,7 @@ async def poll_bridge_progress(self, tx_result: BridgeTxResult) -> IOResult[Brid async def _prepare_eth_tx( self, value: int, - to: Address, + to: ChecksumAddress, source_chain: ChainID, target_chain: ChainID, ) -> PreparedBridgeTx: @@ -169,9 +171,9 @@ async def _prepare_eth_tx( signed_tx = sign_tx(w3=w3, tx=tx, private_key=self.private_key) tx_details = BridgeTxDetails( - contract=func.address, - method=func.fn_name, - kwargs=func.kwargs, + contract=ChecksumAddress(func.address), + fn_name=func.fn_name, + fn_kwargs=func.kwargs, tx=tx, signed_tx=signed_tx, ) @@ -209,7 +211,7 @@ async def _send_bridge_tx(self, prepared_tx: PreparedBridgeTx) -> BridgeTxResult return tx_result - async def _confirm_source_tx(self, tx_result: BridgeTxResult) -> TxReceipt: + async def _confirm_source_tx(self, tx_result: BridgeTxResult) -> TypedTxReceipt: msg = "⏳ Checking source chain [%s] tx receipt for %s" self.logger.info(msg, tx_result.source_chain.name, tx_result.source_tx.tx_hash) @@ -222,14 +224,16 @@ async def _confirm_source_tx(self, tx_result: BridgeTxResult) -> TxReceipt: return tx_receipt - async def _wait_for_target_event(self, tx_result: BridgeTxResult) -> HexBytes: + async def _wait_for_target_event(self, tx_result: BridgeTxResult) -> TxHash: event_log = await self._fetch_standard_event_log(tx_result) - tx_hash = event_log["transactionHash"] + tx_hash = event_log.transactionHash self.logger.info(f"Target event tx_hash found: {tx_hash.to_0x_hex()}") - return tx_hash + return TxHash(tx_hash) + + async def _confirm_target_tx(self, tx_result: BridgeTxResult) -> TypedTxReceipt: + assert tx_result.target_tx is not None, "Expected tx_result.target_tx to exist" - async def _confirm_target_tx(self, tx_result: BridgeTxResult) -> TxReceipt: msg = "⏳ Checking target chain [%s] tx receipt for %s" self.logger.info(msg, tx_result.target_chain.name, tx_result.target_tx.tx_hash) @@ -242,7 +246,9 @@ async def _confirm_target_tx(self, tx_result: BridgeTxResult) -> TxReceipt: return tx_receipt - async def _fetch_standard_event_log(self, tx_result: BridgeTxResult) -> LogReceipt: + async def _fetch_standard_event_log(self, tx_result: BridgeTxResult) -> TypedLogReceipt: + assert tx_result.source_tx.tx_receipt, "Expected source_tx.receipt to exist" + source_event = self.l1_messenger_proxy.events.SentMessage() target_w3 = self.w3s[tx_result.target_chain] @@ -259,7 +265,7 @@ async def _fetch_standard_event_log(self, tx_result: BridgeTxResult) -> LogRecei sender = AsyncWeb3.to_checksum_address(args["sender"]) target = AsyncWeb3.to_checksum_address(args["target"]) message = args["message"] - value = tx_result.amount + value = tx_result.prepared_tx.amount func = self.l1_messenger_proxy.functions.relayMessage( _nonce=nonce, @@ -274,8 +280,8 @@ async def _fetch_standard_event_log(self, tx_result: BridgeTxResult) -> LogRecei tx_result.event_id = msg_hash.hex() self.logger.info(f"🗝️ Computed msgHash: {tx_result.event_id}") - target_event = self.l2_messenger_proxy.events.RelayedMessage() - failed_target_event = self.l2_messenger_proxy.events.FailedRelayedMessage() + target_event = cast(AsyncContractEvent, self.l2_messenger_proxy.events.RelayedMessage()) + failed_target_event = cast(AsyncContractEvent, self.l2_messenger_proxy.events.FailedRelayedMessage()) filter_params = make_filter_params( event=target_event, diff --git a/derive_client/_bridge/async_client.py b/derive_client/_bridge/async_client.py index 400503e5..2e09352b 100644 --- a/derive_client/_bridge/async_client.py +++ b/derive_client/_bridge/async_client.py @@ -3,16 +3,16 @@ from decimal import Decimal from logging import Logger -from eth_account import Account +from eth_account.signers.local import LocalAccount from returns.io import IOResult from derive_client._bridge._derive_bridge import DeriveBridge from derive_client._bridge._standard_bridge import StandardBridge from derive_client.data_types import ( - Address, BridgeTxResult, BridgeType, ChainID, + ChecksumAddress, Currency, Environment, PreparedBridgeTx, @@ -31,7 +31,7 @@ class AsyncBridgeClient: - Multiple chains: BASE, ARBITRUM, OPTIMISM, ETH """ - def __init__(self, env: Environment, account: Account, wallet: Address, logger: Logger): + def __init__(self, env: Environment, account: LocalAccount, wallet: ChecksumAddress, logger: Logger): self._env = env self._account = account self._wallet = wallet @@ -58,10 +58,11 @@ async def connect(self) -> None: self._derive_bridge = derive_bridge self._standard_bridge = StandardBridge(account=self._account, logger=self._logger) - def _ensure_bridge_available(self) -> None: - if self._derive_bridge and self._standard_bridge: - return - raise NotConnectedError("BridgeClient not connected. Call await .connect() first.") + def _require_bridges(self) -> tuple[DeriveBridge, StandardBridge]: + """Return non-None bridges or raise. Keeps attributes private and typed.""" + if self._derive_bridge is None or self._standard_bridge is None: + raise NotConnectedError("BridgeClient not connected. Call await .connect() first.") + return self._derive_bridge, self._standard_bridge # === PUBLIC API (Simple - raises on error) === async def prepare_deposit_tx( @@ -144,11 +145,11 @@ async def try_prepare_gas_deposit_tx( ) -> IOResult[PreparedBridgeTx, Exception]: """Prepare gas deposit with explicit error handling.""" - self._ensure_bridge_available() - to = self._account.address + _, standard_bridge = self._require_bridges() + to = ChecksumAddress(self._account.address) target_chain = ChainID.DERIVE - return await self._standard_bridge.prepare_eth_tx( + return await standard_bridge.prepare_eth_tx( amount=amount, to=to, source_chain=chain_id, @@ -164,8 +165,8 @@ async def try_prepare_deposit_tx( ) -> IOResult[PreparedBridgeTx, Exception]: """Prepare deposit with explicit error handling.""" - self._ensure_bridge_available() - return await self._derive_bridge.prepare_deposit( + derive_bridge, _ = self._require_bridges() + return await derive_bridge.prepare_deposit( amount=amount, currency=currency, chain_id=chain_id, @@ -180,8 +181,8 @@ async def try_prepare_withdrawal_tx( ) -> IOResult[PreparedBridgeTx, Exception]: """Prepare withdrawal with explicit error handling.""" - self._ensure_bridge_available() - return await self._derive_bridge.prepare_withdrawal( + derive_bridge, _ = self._require_bridges() + return await derive_bridge.prepare_withdrawal( amount=amount, currency=currency, chain_id=chain_id, @@ -190,17 +191,17 @@ async def try_prepare_withdrawal_tx( async def try_submit_tx(self, *, prepared_tx: PreparedBridgeTx) -> IOResult[BridgeTxResult, Exception]: """Submit transaction with explicit error handling.""" - self._ensure_bridge_available() + derive_bridge, standard_bridge = self._require_bridges() if prepared_tx.bridge_type == BridgeType.STANDARD: - return await self._standard_bridge.submit_bridge_tx(prepared_tx=prepared_tx) + return await standard_bridge.submit_bridge_tx(prepared_tx=prepared_tx) - return await self._derive_bridge.submit_bridge_tx(prepared_tx=prepared_tx) + return await derive_bridge.submit_bridge_tx(prepared_tx=prepared_tx) async def try_poll_tx_progress(self, *, tx_result: BridgeTxResult) -> IOResult[BridgeTxResult, Exception]: """Poll progress with explicit error handling.""" - self._ensure_bridge_available() + derive_bridge, standard_bridge = self._require_bridges() if tx_result.bridge_type == BridgeType.STANDARD: - return await self._standard_bridge.poll_bridge_progress(tx_result=tx_result) + return await standard_bridge.poll_bridge_progress(tx_result=tx_result) - return await self._derive_bridge.poll_bridge_progress(tx_result=tx_result) + return await derive_bridge.poll_bridge_progress(tx_result=tx_result) diff --git a/derive_client/_bridge/client.py b/derive_client/_bridge/client.py index f125a06d..796a619f 100644 --- a/derive_client/_bridge/client.py +++ b/derive_client/_bridge/client.py @@ -3,16 +3,16 @@ from decimal import Decimal from logging import Logger -from eth_account import Account +from eth_account.signers.local import LocalAccount from returns.io import IOResult from derive_client._bridge._derive_bridge import DeriveBridge from derive_client._bridge._standard_bridge import StandardBridge from derive_client.data_types import ( - Address, BridgeTxResult, BridgeType, ChainID, + ChecksumAddress, Currency, Environment, PreparedBridgeTx, @@ -32,7 +32,7 @@ class BridgeClient: - Multiple chains: BASE, ARBITRUM, OPTIMISM, ETH """ - def __init__(self, env: Environment, account: Account, wallet: Address, logger: Logger): + def __init__(self, env: Environment, account: LocalAccount, wallet: ChecksumAddress, logger: Logger): self._env = env self._account = account self._wallet = wallet @@ -59,10 +59,11 @@ def connect(self) -> None: self._derive_bridge = derive_bridge self._standard_bridge = StandardBridge(account=self._account, logger=self._logger) - def _ensure_bridge_available(self) -> None: - if self._derive_bridge and self._standard_bridge: - return - raise NotConnectedError("BridgeClient not connected. Call await .connect() first.") + def _require_bridges(self) -> tuple[DeriveBridge, StandardBridge]: + """Return non-None bridges or raise. Keeps attributes private and typed.""" + if self._derive_bridge is None or self._standard_bridge is None: + raise NotConnectedError("BridgeClient not connected. Call await .connect() first.") + return self._derive_bridge, self._standard_bridge # === PUBLIC API (Simple - raises on error) === def prepare_deposit_tx( @@ -145,12 +146,12 @@ def try_prepare_gas_deposit_tx( ) -> IOResult[PreparedBridgeTx, Exception]: """Prepare gas deposit with explicit error handling.""" - self._ensure_bridge_available() - to = self._account.address + _, standard_bridge = self._require_bridges() + to = ChecksumAddress(self._account.address) target_chain = ChainID.DERIVE return run_coroutine_sync( - self._standard_bridge.prepare_eth_tx( + standard_bridge.prepare_eth_tx( amount=amount, to=to, source_chain=chain_id, @@ -167,9 +168,9 @@ def try_prepare_deposit_tx( ) -> IOResult[PreparedBridgeTx, Exception]: """Prepare deposit with explicit error handling.""" - self._ensure_bridge_available() + derive_bridge, _ = self._require_bridges() return run_coroutine_sync( - self._derive_bridge.prepare_deposit( + derive_bridge.prepare_deposit( amount=amount, currency=currency, chain_id=chain_id, @@ -185,9 +186,9 @@ def try_prepare_withdrawal_tx( ) -> IOResult[PreparedBridgeTx, Exception]: """Prepare withdrawal with explicit error handling.""" - self._ensure_bridge_available() + derive_bridge, _ = self._require_bridges() return run_coroutine_sync( - self._derive_bridge.prepare_withdrawal( + derive_bridge.prepare_withdrawal( amount=amount, currency=currency, chain_id=chain_id, @@ -197,17 +198,17 @@ def try_prepare_withdrawal_tx( def try_submit_tx(self, *, prepared_tx: PreparedBridgeTx) -> IOResult[BridgeTxResult, Exception]: """Submit transaction with explicit error handling.""" - self._ensure_bridge_available() + derive_bridge, standard_bridge = self._require_bridges() if prepared_tx.bridge_type == BridgeType.STANDARD: - return run_coroutine_sync(self._standard_bridge.submit_bridge_tx(prepared_tx=prepared_tx)) + return run_coroutine_sync(standard_bridge.submit_bridge_tx(prepared_tx=prepared_tx)) - return run_coroutine_sync(self._derive_bridge.submit_bridge_tx(prepared_tx=prepared_tx)) + return run_coroutine_sync(derive_bridge.submit_bridge_tx(prepared_tx=prepared_tx)) def try_poll_tx_progress(self, *, tx_result: BridgeTxResult) -> IOResult[BridgeTxResult, Exception]: """Poll progress with explicit error handling.""" - self._ensure_bridge_available() + derive_bridge, standard_bridge = self._require_bridges() if tx_result.bridge_type == BridgeType.STANDARD: - return run_coroutine_sync(self._standard_bridge.poll_bridge_progress(tx_result=tx_result)) + return run_coroutine_sync(standard_bridge.poll_bridge_progress(tx_result=tx_result)) - return run_coroutine_sync(self._derive_bridge.poll_bridge_progress(tx_result=tx_result)) + return run_coroutine_sync(derive_bridge.poll_bridge_progress(tx_result=tx_result)) diff --git a/derive_client/_bridge/w3.py b/derive_client/_bridge/w3.py index 571e4e12..663b1972 100644 --- a/derive_client/_bridge/w3.py +++ b/derive_client/_bridge/w3.py @@ -4,18 +4,18 @@ import statistics import time from logging import Logger -from typing import Any, Callable, Generator, Literal +from typing import Any, AsyncGenerator, Callable, Coroutine, Literal, cast -from eth_abi import encode +from eth_abi.abi import encode from eth_account import Account -from eth_account.datastructures import SignedTransaction +from eth_account.signers.local import LocalAccount +from eth_typing import HexStr from requests import RequestException from web3 import AsyncHTTPProvider, AsyncWeb3 -from web3.contract import Contract from web3.contract.async_contract import AsyncContract, AsyncContractEvent, AsyncContractFunction -from web3.datastructures import AttributeDict +from web3.types import RPCEndpoint, RPCError, RPCResponse -from derive_client.constants import ( +from derive_client.config import ( ABI_DATA_DIR, ASSUMED_BRIDGE_GAS_LIMIT, DEFAULT_RPC_ENDPOINTS, @@ -23,14 +23,20 @@ MIN_PRIORITY_FEE, ) from derive_client.data_types import ( - Address, ChainID, + ChecksumAddress, FeeEstimate, FeeEstimates, FeeHistory, GasPriority, RPCEndpoints, + TxHash, TxStatus, + TypedFilterParams, + TypedLogReceipt, + TypedSignedTransaction, + TypedTransaction, + TypedTxReceipt, Wei, ) from derive_client.exceptions import ( @@ -55,7 +61,7 @@ def make_rotating_provider_middleware( initial_backoff: float = 1.0, max_backoff: float = 600.0, logger: Logger, -) -> Callable[[Callable[[str, Any], Any], AsyncWeb3], Callable[[str, Any], Any]]: +) -> Callable[[Callable[[RPCEndpoint, Any], RPCResponse], AsyncWeb3], Any]: """ v6.11-style middleware: - round-robin via a min-heap of `next_available` times @@ -66,8 +72,11 @@ def make_rotating_provider_middleware( heapq.heapify(heap) lock = asyncio.Lock() - async def middleware_factory(make_request: Callable[[str, Any], Any], w3: AsyncWeb3) -> Callable[[str, Any], Any]: - async def rotating_backoff(method: str, params: Any) -> Any: + async def middleware_factory( + make_request: Callable[[RPCEndpoint, Any], RPCResponse], + w3: AsyncWeb3, + ) -> Callable[[RPCEndpoint, Any], Coroutine[Any, Any, RPCResponse]]: + async def rotating_backoff(method: RPCEndpoint, params: Any) -> RPCResponse: now = time.monotonic() while True: @@ -94,9 +103,14 @@ async def rotating_backoff(method: str, params: Any) -> Any: state.next_available = now + state.backoff async with lock: heapq.heappush(heap, state) - err_msg = error.get("message", "") - err_code = error.get("code", "") - msg = "RPC error on %s: %s (code: %s)→ backing off %.2fs" + + if isinstance(error, str): + msg = error + else: + err_msg = error.get("message", "") + err_code = error.get("code", "") + msg = "RPC error on %s: %s (code: %s)→ backing off %.2fs" + logger.info(msg, state.provider.endpoint_uri, err_msg, err_code, state.backoff) continue @@ -111,10 +125,10 @@ async def rotating_backoff(method: str, params: Any) -> Any: logger.debug("Endpoint %s failed: %s", state.provider.endpoint_uri, e) # We retry on all exceptions - hdr = (e.response and e.response.headers or {}).get("Retry-After") - try: + hdr = (e.response.headers if e.response else {}).get("Retry-After") + if hdr is not None: backoff = float(hdr) - except (ValueError, TypeError): + else: backoff = state.backoff * 2 if state.backoff > 0 else initial_backoff # cap backoff and schedule @@ -170,17 +184,22 @@ def get_w3_connections(logger) -> dict[ChainID, AsyncWeb3]: return {chain_id: get_w3_connection(chain_id, logger=logger) for chain_id in ChainID} -def get_contract(w3: AsyncWeb3, address: str, abi: list) -> AsyncContract: +def get_contract(w3: AsyncWeb3, address: ChecksumAddress, abi: list) -> AsyncContract: return w3.eth.contract(address=AsyncWeb3.to_checksum_address(address), abi=abi) -def get_erc20_contract(w3: AsyncWeb3, token_address: str) -> AsyncContract: +def get_erc20_contract(w3: AsyncWeb3, token_address: ChecksumAddress) -> AsyncContract: erc20_abi_path = ABI_DATA_DIR / "erc20.json" abi = json.loads(erc20_abi_path.read_text()) return get_contract(w3=w3, address=token_address, abi=abi) -async def ensure_token_balance(token_contract: Contract, owner: Address, amount: int, fee_in_token: int = 0): +async def ensure_token_balance( + token_contract: AsyncContract, + owner: ChecksumAddress, + amount: int, + fee_in_token: int = 0, +): balance = await token_contract.functions.balanceOf(owner).call() required = amount + fee_in_token if amount > balance: @@ -192,9 +211,9 @@ async def ensure_token_balance(token_contract: Contract, owner: Address, amount: async def ensure_token_allowance( w3: AsyncWeb3, - token_contract: Contract, - owner: Address, - spender: Address, + token_contract: AsyncContract, + owner: ChecksumAddress, + spender: ChecksumAddress, amount: int, private_key: str, logger: Logger, @@ -215,9 +234,9 @@ async def ensure_token_allowance( async def _increase_token_allowance( w3: AsyncWeb3, - from_account: Account, - erc20_contract: Contract, - spender: Address, + from_account: LocalAccount, + erc20_contract: AsyncContract, + spender: ChecksumAddress, amount: int, private_key: str, logger: Logger, @@ -238,7 +257,7 @@ async def estimate_fees(w3, blocks: int = 20) -> FeeEstimates: fee_history = FeeHistory(**await w3.eth.fee_history(blocks, "pending", percentiles)) latest_base_fee = fee_history.base_fee_per_gas[-1] - percentile_rewards = {p: [] for p in percentiles} + percentile_rewards: dict[int, list[Wei]] = {p: [] for p in percentiles} for block_rewards in fee_history.reward: for percentile, reward in zip(percentiles, block_rewards): percentile_rewards[percentile].append(reward) @@ -251,7 +270,7 @@ async def estimate_fees(w3, blocks: int = 20) -> FeeEstimates: buffered_base_fee = int(latest_base_fee * GAS_FEE_BUFFER) estimated_max_fee = buffered_base_fee + estimated_priority_fee - estimates[percentile] = FeeEstimate(estimated_max_fee, estimated_priority_fee) + estimates[GasPriority(percentile)] = FeeEstimate(estimated_max_fee, estimated_priority_fee) return FeeEstimates(estimates) @@ -259,8 +278,8 @@ async def estimate_fees(w3, blocks: int = 20) -> FeeEstimates: async def preflight_native_balance_check( w3: AsyncWeb3, fee_estimate: FeeEstimate, - account: Account, - value: Wei, + account: LocalAccount, + value: int, ) -> None: balance = await w3.eth.get_balance(account.address) max_fee_per_gas = fee_estimate.max_fee_per_gas @@ -285,7 +304,7 @@ async def preflight_native_balance_check( @exp_backoff_retry async def build_standard_transaction( func, - account: Account, + account: LocalAccount, w3: AsyncWeb3, logger: Logger, value: int = 0, @@ -333,7 +352,7 @@ async def wait_for_tx_finality( finality_blocks: int = 10, timeout: float = 300.0, poll_interval: float = 1.0, -) -> AttributeDict: +) -> TypedTxReceipt: """ Wait until tx is mined and has `finality_blocks` confirmations. On timeout this raises one of: @@ -357,11 +376,13 @@ async def wait_for_tx_finality( either wait/poll longer or resubmit (reuse the nonce to prevent duplication). """ + block_number = -1 + tx_hash = cast(HexStr, tx_hash) start_time = time.monotonic() while True: try: - receipt = AttributeDict(await w3.eth.get_transaction_receipt(tx_hash)) + receipt = TypedTxReceipt.model_validate(await w3.eth.get_transaction_receipt(tx_hash)) # receipt can disappear temporarily during reorgs, or if RPC provider is not synced except Exception as exc: receipt = None @@ -369,11 +390,10 @@ async def wait_for_tx_finality( # blockNumber can change as tx gets reorged into different blocks try: - if ( - receipt is not None - and (block_number := await w3.eth.block_number) >= receipt.blockNumber + finality_blocks - ): - return receipt + if receipt is not None: + block_number = await w3.eth.block_number + if block_number >= receipt.blockNumber + finality_blocks: + return receipt except Exception as exc: msg = "Failed to fetch block_number trying to assess finality of tx_hash=%s" logger.debug(msg, tx_hash, extra={"exc": exc}) @@ -389,7 +409,7 @@ async def wait_for_tx_finality( ) # 2) No receipt: check if tx is known to node (mempool) or dropped try: - tx = AttributeDict(w3.eth.get_transaction(tx_hash)) + tx = TypedTransaction.model_validate(await w3.eth.get_transaction(tx_hash)) except Exception as exc: tx = None logger.debug("get_transaction probe failed for tx_hash=%s", tx_hash, extra={"exc": exc}) @@ -421,53 +441,59 @@ async def wait_for_tx_finality( await asyncio.sleep(poll_interval) -def sign_tx(w3: AsyncWeb3, tx: dict, private_key: str) -> SignedTransaction: +def sign_tx(w3: AsyncWeb3, tx: dict, private_key: str) -> TypedSignedTransaction: signed_tx = w3.eth.account.sign_transaction(tx, private_key=private_key) - return signed_tx + return TypedSignedTransaction(**signed_tx) -async def send_tx(w3: AsyncWeb3, signed_tx: SignedTransaction) -> str: +async def send_tx(w3: AsyncWeb3, signed_tx: TypedSignedTransaction) -> TxHash: tx_hash = await w3.eth.send_raw_transaction(signed_tx.raw_transaction) - return tx_hash.to_0x_hex() + return TxHash(tx_hash) async def iter_events( w3: AsyncWeb3, - filter_params: dict, + filter_params: TypedFilterParams, *, - condition: Callable[[AttributeDict], bool] = lambda _: True, + condition: Callable[[TypedLogReceipt], bool] = lambda _: True, max_block_range: int = 10_000, poll_interval: float = 5.0, timeout: float | None = None, logger: Logger, -) -> Generator[AttributeDict, None, None]: +) -> AsyncGenerator[TypedLogReceipt, None]: """Stream matching logs over a fixed or live block window. Optionally raises TimeoutError.""" - original_filter_params = filter_params.copy() # return original in TimeoutError - if (cursor := filter_params["fromBlock"]) == "latest": + if (cursor := filter_params.fromBlock) == "latest": cursor = await w3.eth.block_number start_block = cursor - filter_params["toBlock"] = filter_params.get("toBlock", "latest") - fixed_ceiling = None if filter_params["toBlock"] == "latest" else filter_params["toBlock"] + fixed_ceiling = None if filter_params.toBlock == "latest" else filter_params.toBlock + rpc_filter_params = filter_params.to_rpc_params() deadline = None if timeout is None else time.monotonic() + timeout + while True: if deadline and time.monotonic() > deadline: msg = f"Timed out waiting for events after scanning blocks {start_block}-{cursor}" logger.warning(msg) - raise TimeoutError(f"{msg}: filter_params: {original_filter_params}") + raise TimeoutError(f"{msg}: filter_params: {filter_params}") + upper = fixed_ceiling or await w3.eth.block_number if cursor <= upper: end = min(upper, cursor + max_block_range - 1) - filter_params["fromBlock"] = hex(cursor) - filter_params["toBlock"] = hex(end) + + # Convert to hex strings for RPC call - some providers require this + rpc_filter_params["fromBlock"] = cast(HexStr, hex(cursor)) + rpc_filter_params["toBlock"] = cast(HexStr, hex(end)) + # For example, when rotating providers are out of sync retry_get_logs = exp_backoff_retry(w3.eth.get_logs, attempts=EVENT_LOG_RETRIES) - logs = await retry_get_logs(filter_params=filter_params) - logger.debug(f"Scanned {cursor} - {end}: {len(logs)} logs") - for log in filter(condition, logs): + logs_raw = await retry_get_logs(filter_params=rpc_filter_params) + logger.debug(f"Scanned {cursor} - {end}: {len(logs_raw)} logs") + + for log in filter(condition, map(TypedLogReceipt.model_validate, logs_raw)): yield log + cursor = end + 1 # bounds are inclusive if fixed_ceiling and cursor > fixed_ceiling: @@ -478,14 +504,14 @@ async def iter_events( async def wait_for_bridge_event( w3: AsyncWeb3, - filter_params: dict, + filter_params: TypedFilterParams, *, - condition: Callable[[AttributeDict], bool] = lambda _: True, + condition: Callable[[TypedLogReceipt], bool] = lambda _: True, max_block_range: int = 10_000, poll_interval: float = 5.0, timeout: float = 300.0, logger: Logger, -) -> AttributeDict: +) -> TypedLogReceipt: """Wait for the first matching bridge-related log on the target chain or raise BridgeEventTimeout.""" try: @@ -498,36 +524,45 @@ def make_filter_params( event: AsyncContractEvent, from_block: int | Literal["latest"], to_block: int | Literal["latest"] = "latest", - argument_filters: dict | None = None, -) -> dict: + argument_filters: dict[str, Any] | None = None, +) -> TypedFilterParams: """ Function to create an eth_getLogs compatible filter_params for this event without using .create_filter. event.create_filter uses eth_newFilter (a "push"), which not all RPC endpoints support. """ argument_filters = argument_filters or {} - filter_params = event._get_event_filter_params( + + filter_params_raw = event._get_event_filter_params( fromBlock=from_block, toBlock=to_block, argument_filters=argument_filters, abi=event.abi, ) - filter_params["topics"] = tuple(filter_params["topics"]) - address = filter_params["address"] - if isinstance(address, str): - filter_params["address"] = AsyncWeb3.to_checksum_address(address) - elif isinstance(address, (list, tuple)) and len(address) == 1: - filter_params["address"] = AsyncWeb3.to_checksum_address(address[0]) + + address_raw = filter_params_raw["address"] + address: ChecksumAddress | list[ChecksumAddress] + + address = filter_params_raw["address"] + if isinstance(address_raw, str): + address = ChecksumAddress(address_raw) + elif isinstance(address_raw, (list, tuple)) and len(address_raw) == 1: + address = ChecksumAddress(address_raw[0]) else: raise ValueError(f"Unexpected address filter: {address!r}") - return filter_params + return TypedFilterParams( + address=address, + topics=tuple(filter_params_raw["topics"]), + fromBlock=from_block, + toBlock=to_block, + ) def encode_abi(func: AsyncContractFunction) -> bytes: """Get the ABI-encoded data (including 4-byte selector).""" - types = [arg["internalType"] for arg in func.abi["inputs"]] + types = [str(arg.get("internalType")) for arg in func.abi.get("inputs", [])] selector = bytes.fromhex(func.selector.removeprefix("0x")) return selector + encode(types, func.arguments) From e5f1c930ee91d5f585360f742e348e90180755ff Mon Sep 17 00:00:00 2001 From: zarathustra Date: Sun, 2 Nov 2025 17:25:08 +0100 Subject: [PATCH 18/22] feat: fully-typed cli tool --- derive_client/cli/_bridge.py | 37 +++++++++++++++++++----------- derive_client/cli/_context.py | 22 ++++++++++++++---- derive_client/cli/_markets.py | 2 +- derive_client/cli/_orders.py | 2 +- derive_client/cli/_transactions.py | 14 +++-------- derive_client/cli/_utils.py | 2 +- 6 files changed, 46 insertions(+), 33 deletions(-) diff --git a/derive_client/cli/_bridge.py b/derive_client/cli/_bridge.py index 45bf8e6e..98dd5850 100644 --- a/derive_client/cli/_bridge.py +++ b/derive_client/cli/_bridge.py @@ -3,6 +3,8 @@ from __future__ import annotations from decimal import Decimal +from enum import Enum +from typing import Type, TypeVar import rich_click as click from rich import print @@ -16,6 +18,20 @@ from ._utils import rich_prepared_tx +E = TypeVar("E", bound=Enum) + + +class EnumChoice(click.Choice): + """Click choice type that converts to enum.""" + + def __init__(self, enum_type: Type[E], case_sensitive: bool = False): + self.enum_type = enum_type + super().__init__([e.name for e in enum_type], case_sensitive=case_sensitive) + + def convert(self, value, param, ctx): + name = super().convert(value, param, ctx) + return self.enum_type[name] + @click.group("bridge") @click.pass_context @@ -27,14 +43,14 @@ def bridge(ctx): @click.option( "--chain-id", "-c", - type=click.Choice([c.name for c in ChainID]), + type=EnumChoice(ChainID), required=True, help="The chain ID to bridge FROM.", ) @click.option( "--currency", "-t", - type=click.Choice([c.name for c in Currency]), + type=EnumChoice(Currency), required=True, help="The token symbol (e.g. weETH) to bridge.", ) @@ -46,7 +62,7 @@ def bridge(ctx): help="The amount to deposit in decimal units of the selected token (converted to base units internally).", ) @click.pass_context -def deposit(ctx, chain_id, currency, amount): +def deposit(ctx, chain_id: ChainID, currency: Currency, amount: Decimal): """ Deposit funds via the socket superbridge to a Derive funding account. @@ -57,9 +73,6 @@ def deposit(ctx, chain_id, currency, amount): client: HTTPClient = ctx.obj["client"] bridge: BridgeClient = client.bridge - chain_id = ChainID[chain_id] - currency = Currency[currency] - prepared_tx = bridge.prepare_deposit_tx(chain_id=chain_id, currency=currency, amount=amount) print(rich_prepared_tx(prepared_tx)) @@ -89,13 +102,13 @@ def deposit(ctx, chain_id, currency, amount): ) @click.option( "--chain-id", - type=click.Choice([ChainID.ETH.name]), - default=ChainID.ETH.name, + type=EnumChoice(ChainID), + default=ChainID.ETH, show_default=True, required=True, ) @click.pass_context -def gas(ctx, amount, chain_id): +def gas(ctx, amount: Decimal, chain_id: ChainID): """Deposit gas (native token) for bridging.""" click.echo(f"Deposit gas: chain={chain_id} amount={amount}") @@ -109,7 +122,6 @@ def gas(ctx, amount, chain_id): client: HTTPClient = ctx.obj["client"] bridge: BridgeClient = client.bridge - chain_id = ChainID[chain_id] currency = Currency.ETH prepared_tx = bridge.prepare_gas_deposit_tx(amount=amount, chain_id=chain_id) @@ -156,7 +168,7 @@ def gas(ctx, amount, chain_id): help="The amount to withdraw in human units of the selected token (converted to base units internally).", ) @click.pass_context -def withdraw(ctx, chain_id, currency, amount): +def withdraw(ctx, chain_id: ChainID, currency: Currency, amount: Decimal): """ Withdraw funds from Derive funding account via the Withdraw Wrapper contract. @@ -167,9 +179,6 @@ def withdraw(ctx, chain_id, currency, amount): client: HTTPClient = ctx.obj["client"] bridge: BridgeClient = client.bridge - chain_id = ChainID[chain_id] - currency = Currency[currency] - prepared_tx = bridge.prepare_withdrawal_tx(chain_id=chain_id, currency=currency, amount=amount) print(rich_prepared_tx(prepared_tx)) diff --git a/derive_client/cli/_context.py b/derive_client/cli/_context.py index b94b0db3..511d6b8a 100644 --- a/derive_client/cli/_context.py +++ b/derive_client/cli/_context.py @@ -9,7 +9,7 @@ from dotenv import load_dotenv from derive_client._clients.rest.http.client import HTTPClient -from derive_client.data_types import Environment +from derive_client.data_types import ChecksumAddress, Environment def create_client( @@ -23,16 +23,16 @@ def create_client( load_dotenv(dotenv_path=dotenv_path) session_key = session_key_path.read_text().strip() if session_key_path else os.environ.get("DERIVE_SESSION_KEY") - wallet = os.environ.get("DERIVE_WALLET") - subaccount_id = os.environ.get("DERIVE_SUBACCOUNT_ID") + wallet_str = os.environ.get("DERIVE_WALLET") + subaccount_id_str = os.environ.get("DERIVE_SUBACCOUNT_ID") env = Environment[os.environ.get("DERIVE_ENV", "PROD").upper()] missing = [] if not session_key: missing.append("DERIVE_SESSION_KEY: Not found in environment variables or via --session-key-path flag") - if not wallet: + if not wallet_str: missing.append("DERIVE_WALLET: Not found in environment variables.") - if not subaccount_id: + if not subaccount_id_str: missing.append("DERIVE_SUBACCOUNT_ID: Not found in environment variables.") if missing: @@ -50,6 +50,18 @@ def create_client( raise click.ClickException(error_msg) + assert session_key and wallet_str and subaccount_id_str, "type-checker" + + try: + wallet = ChecksumAddress(wallet_str) + except ValueError as e: + raise click.ClickException(f"Invalid wallet address: {e}") + + try: + subaccount_id = int(subaccount_id_str) + except ValueError: + raise click.ClickException(f"Invalid subaccount ID '{subaccount_id_str}': must be an integer") + return HTTPClient( wallet=wallet, session_key=session_key, diff --git a/derive_client/cli/_markets.py b/derive_client/cli/_markets.py index fbad112e..42fa28ce 100644 --- a/derive_client/cli/_markets.py +++ b/derive_client/cli/_markets.py @@ -5,7 +5,7 @@ import pandas as pd import rich_click as click -from derive_client.data.generated.models import InstrumentType +from derive_client.data_types import InstrumentType from ._columns import CURRENCY_COLUMNS, INSTRUMENT_COLUMNS from ._utils import struct_to_series, structs_to_dataframe diff --git a/derive_client/cli/_orders.py b/derive_client/cli/_orders.py index d63d311a..b2f11fdf 100644 --- a/derive_client/cli/_orders.py +++ b/derive_client/cli/_orders.py @@ -6,7 +6,7 @@ import rich_click as click -from derive_client.data.generated.models import Direction, OrderType +from derive_client.data_types import Direction, OrderType from ._columns import ORDER_COLUMNS, TRADE_COLUMNS from ._utils import struct_to_series, structs_to_dataframe diff --git a/derive_client/cli/_transactions.py b/derive_client/cli/_transactions.py index ef47e2fd..bba81e53 100644 --- a/derive_client/cli/_transactions.py +++ b/derive_client/cli/_transactions.py @@ -4,7 +4,6 @@ from decimal import Decimal -import pandas as pd import rich_click as click from ._utils import struct_to_series @@ -29,18 +28,11 @@ def get(ctx, transaction_id: str): subaccount = client.active_subaccount transaction = subaccount.transactions.get(transaction_id=transaction_id) - tx_data = dict( - subaccount_id=transaction.data.subaccount_id, - status=transaction.status.name, - asset_name=transaction.data.asset_name, - amount=transaction.data.data.amount, - ) - series = pd.Series(tx_data) - print("\n=== Transaction ===") - print(series.to_string(index=True)) + print(f"Status: {transaction.status.name}") + print(f"Tx Hash: {transaction.status.name}") if transaction.error_log: - print(f"\nError: {transaction.error_log.error}") + print(f"\nError: {transaction.error_log}") @transaction.command("deposit-to-subaccount") diff --git a/derive_client/cli/_utils.py b/derive_client/cli/_utils.py index d0c0c75a..5c11f52b 100644 --- a/derive_client/cli/_utils.py +++ b/derive_client/cli/_utils.py @@ -37,7 +37,7 @@ def rich_prepared_tx(prepared_tx: PreparedBridgeTx): fee_human = from_base_units(prepared_tx.fee_in_token, prepared_tx.currency) table.add_row( "Estimated fee (token)", - f"{fmt_sig_up_to(fee_human)} {prepared_tx.currency.name} (base units: {prepared_tx.fee_in_token})", + f"{fee_human} {prepared_tx.currency.name} (base units: {prepared_tx.fee_in_token})", ) if prepared_tx.value and prepared_tx.value > 0: human_value = prepared_tx.value / 1e18 From 6b4b3c3b9af8dcf02635e6ef51c95722f8068eb3 Mon Sep 17 00:00:00 2001 From: zarathustra Date: Sun, 2 Nov 2025 17:42:52 +0100 Subject: [PATCH 19/22] feat: add both mypy and pyright because centralized type checking is ngmi --- derive_client/__init__.py | 11 ++-- derive_client/analyser.py | 72 ------------------------- derive_client/exceptions.py | 12 ++--- poetry.lock | 102 +++++++++++++++++++++++++++++++++++- pyproject.toml | 17 ++++++ 5 files changed, 129 insertions(+), 85 deletions(-) delete mode 100644 derive_client/analyser.py diff --git a/derive_client/__init__.py b/derive_client/__init__.py index b7c9956d..3f0e52d7 100644 --- a/derive_client/__init__.py +++ b/derive_client/__init__.py @@ -1,7 +1,8 @@ -""" -Init for the derive client -""" +"""Derive client package.""" -from .derive import DeriveClient +from ._clients import AsyncHTTPClient, HTTPClient -DeriveClient +__all__ = [ + "HTTPClient", + "AsyncHTTPClient", +] diff --git a/derive_client/analyser.py b/derive_client/analyser.py deleted file mode 100644 index dee55e9e..00000000 --- a/derive_client/analyser.py +++ /dev/null @@ -1,72 +0,0 @@ -""" -Class based analyser for portfolios. -""" - -from typing import List, Optional - -import pandas as pd - -pd.set_option('display.precision', 2) - - -DELTA_COLUMNS = ['delta', 'gamma', 'vega', 'theta'] - - -class PortfolioAnalyser: - raw_data: List[dict] - df: pd.DataFrame - - def __init__(self, raw_data: List[dict]): - self.raw_data = raw_data - if not raw_data: - raise ValueError("No data provided") - open_positions = raw_data['positions'] - if open_positions: - self.positions = pd.DataFrame.from_records(raw_data['positions']) - self.positions["amount"] = pd.to_numeric(self.positions["amount"]) - for col in DELTA_COLUMNS: - self.positions[col] = pd.to_numeric(self.positions[col]) - adjusted_greek = self.positions[col] * self.positions.amount - self.positions[col] = adjusted_greek - - self.positions = self.positions.apply(pd.to_numeric, errors='ignore') - else: - self.positions = pd.DataFrame(columns=['instrument_name', 'amount'] + DELTA_COLUMNS) - - def get_positions(self, underlying_currency: str) -> pd.DataFrame: - df = self.positions - df = df[df['instrument_name'].str.contains(underlying_currency.upper())] - return df - - def get_open_positions(self, underlying_currency: str) -> pd.DataFrame: - df = self.get_positions(underlying_currency) - return df[df['amount'] != 0] - - def get_total_greeks(self, underlying_currency: str) -> pd.DataFrame: - df = self.get_open_positions(underlying_currency) - return df[DELTA_COLUMNS].sum() - - def get_subaccount_value(self) -> float: - return float(self.raw_data['subaccount_value']) - - def print_positions(self, underlying_currency: str, columns: Optional[List[str]] = None): - df = self.get_open_positions(underlying_currency) - if columns: - df = df[[c for c in columns if c not in DELTA_COLUMNS] + DELTA_COLUMNS] - print(df) - - def calculate_greeks_of_option( - self, - underlying_price: float, - strike_price: float, - interest_rate: float, - days_to_expiration: int, - volatility: float, - ) -> dict: - """ - Calculate the greeks of each option position using the Black-Scholes model. - # BS([underlyingPrice, strikePrice, interestRate, daysToExpiration], volatility=x, callPrice=y, putPrice=z) - - # eg: - # c = mibian.BS([1.4565, 1.45, 1, 30], volatility=20) - """ diff --git a/derive_client/exceptions.py b/derive_client/exceptions.py index b2238753..f348c3d1 100644 --- a/derive_client/exceptions.py +++ b/derive_client/exceptions.py @@ -5,9 +5,7 @@ from typing import TYPE_CHECKING, Any if TYPE_CHECKING: - from web3.types import LogReceipt - - from derive_client.data_types import BridgeTxResult, ChainID, FeeEstimate, Wei + from derive_client.data_types import BridgeTxResult, ChainID, FeeEstimate, TypedLogReceipt class NotConnectedError(RuntimeError): @@ -64,8 +62,8 @@ def __init__( message: str, *, chain_id: ChainID, - balance: Wei, - assumed_gas_limit: Wei, + balance: int, + assumed_gas_limit: int, fee_estimate: FeeEstimate, ): super().__init__(message) @@ -111,7 +109,7 @@ def __init__(self, message: str, *, tx_result: BridgeTxResult): self.tx_result = tx_result @property - def cause(self) -> Exception | None: + def cause(self) -> BaseException | None: """Provides access to the orignal Exception.""" return self.__cause__ @@ -119,6 +117,6 @@ def cause(self) -> Exception | None: class StandardBridgeRelayFailed(Exception): """Raised when the L2 messenger emits FailedRelayedMessage.""" - def __init__(self, message: str, *, event_log: LogReceipt): + def __init__(self, message: str, *, event_log: TypedLogReceipt): super().__init__(message) self.event_log = event_log diff --git a/poetry.lock b/poetry.lock index d4999a99..76cce1ed 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2194,6 +2194,66 @@ files = [ {file = "multidict-6.7.0.tar.gz", hash = "sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5"}, ] +[[package]] +name = "mypy" +version = "1.18.2" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "mypy-1.18.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c1eab0cf6294dafe397c261a75f96dc2c31bffe3b944faa24db5def4e2b0f77c"}, + {file = "mypy-1.18.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7a780ca61fc239e4865968ebc5240bb3bf610ef59ac398de9a7421b54e4a207e"}, + {file = "mypy-1.18.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:448acd386266989ef11662ce3c8011fd2a7b632e0ec7d61a98edd8e27472225b"}, + {file = "mypy-1.18.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f9e171c465ad3901dc652643ee4bffa8e9fef4d7d0eece23b428908c77a76a66"}, + {file = "mypy-1.18.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:592ec214750bc00741af1f80cbf96b5013d81486b7bb24cb052382c19e40b428"}, + {file = "mypy-1.18.2-cp310-cp310-win_amd64.whl", hash = "sha256:7fb95f97199ea11769ebe3638c29b550b5221e997c63b14ef93d2e971606ebed"}, + {file = "mypy-1.18.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:807d9315ab9d464125aa9fcf6d84fde6e1dc67da0b6f80e7405506b8ac72bc7f"}, + {file = "mypy-1.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:776bb00de1778caf4db739c6e83919c1d85a448f71979b6a0edd774ea8399341"}, + {file = "mypy-1.18.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1379451880512ffce14505493bd9fe469e0697543717298242574882cf8cdb8d"}, + {file = "mypy-1.18.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1331eb7fd110d60c24999893320967594ff84c38ac6d19e0a76c5fd809a84c86"}, + {file = "mypy-1.18.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ca30b50a51e7ba93b00422e486cbb124f1c56a535e20eff7b2d6ab72b3b2e37"}, + {file = "mypy-1.18.2-cp311-cp311-win_amd64.whl", hash = "sha256:664dc726e67fa54e14536f6e1224bcfce1d9e5ac02426d2326e2bb4e081d1ce8"}, + {file = "mypy-1.18.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:33eca32dd124b29400c31d7cf784e795b050ace0e1f91b8dc035672725617e34"}, + {file = "mypy-1.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a3c47adf30d65e89b2dcd2fa32f3aeb5e94ca970d2c15fcb25e297871c8e4764"}, + {file = "mypy-1.18.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d6c838e831a062f5f29d11c9057c6009f60cb294fea33a98422688181fe2893"}, + {file = "mypy-1.18.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01199871b6110a2ce984bde85acd481232d17413868c9807e95c1b0739a58914"}, + {file = "mypy-1.18.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a2afc0fa0b0e91b4599ddfe0f91e2c26c2b5a5ab263737e998d6817874c5f7c8"}, + {file = "mypy-1.18.2-cp312-cp312-win_amd64.whl", hash = "sha256:d8068d0afe682c7c4897c0f7ce84ea77f6de953262b12d07038f4d296d547074"}, + {file = "mypy-1.18.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:07b8b0f580ca6d289e69209ec9d3911b4a26e5abfde32228a288eb79df129fcc"}, + {file = "mypy-1.18.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ed4482847168439651d3feee5833ccedbf6657e964572706a2adb1f7fa4dfe2e"}, + {file = "mypy-1.18.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3ad2afadd1e9fea5cf99a45a822346971ede8685cc581ed9cd4d42eaf940986"}, + {file = "mypy-1.18.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a431a6f1ef14cf8c144c6b14793a23ec4eae3db28277c358136e79d7d062f62d"}, + {file = "mypy-1.18.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7ab28cc197f1dd77a67e1c6f35cd1f8e8b73ed2217e4fc005f9e6a504e46e7ba"}, + {file = "mypy-1.18.2-cp313-cp313-win_amd64.whl", hash = "sha256:0e2785a84b34a72ba55fb5daf079a1003a34c05b22238da94fcae2bbe46f3544"}, + {file = "mypy-1.18.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:62f0e1e988ad41c2a110edde6c398383a889d95b36b3e60bcf155f5164c4fdce"}, + {file = "mypy-1.18.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8795a039bab805ff0c1dfdb8cd3344642c2b99b8e439d057aba30850b8d3423d"}, + {file = "mypy-1.18.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ca1e64b24a700ab5ce10133f7ccd956a04715463d30498e64ea8715236f9c9c"}, + {file = "mypy-1.18.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d924eef3795cc89fecf6bedc6ed32b33ac13e8321344f6ddbf8ee89f706c05cb"}, + {file = "mypy-1.18.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20c02215a080e3a2be3aa50506c67242df1c151eaba0dcbc1e4e557922a26075"}, + {file = "mypy-1.18.2-cp314-cp314-win_amd64.whl", hash = "sha256:749b5f83198f1ca64345603118a6f01a4e99ad4bf9d103ddc5a3200cc4614adf"}, + {file = "mypy-1.18.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:25a9c8fb67b00599f839cf472713f54249a62efd53a54b565eb61956a7e3296b"}, + {file = "mypy-1.18.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c2b9c7e284ee20e7598d6f42e13ca40b4928e6957ed6813d1ab6348aa3f47133"}, + {file = "mypy-1.18.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d6985ed057513e344e43a26cc1cd815c7a94602fb6a3130a34798625bc2f07b6"}, + {file = "mypy-1.18.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22f27105f1525ec024b5c630c0b9f36d5c1cc4d447d61fe51ff4bd60633f47ac"}, + {file = "mypy-1.18.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:030c52d0ea8144e721e49b1f68391e39553d7451f0c3f8a7565b59e19fcb608b"}, + {file = "mypy-1.18.2-cp39-cp39-win_amd64.whl", hash = "sha256:aa5e07ac1a60a253445797e42b8b2963c9675563a94f11291ab40718b016a7a0"}, + {file = "mypy-1.18.2-py3-none-any.whl", hash = "sha256:22a1748707dd62b58d2ae53562ffc4d7f8bcc727e8ac7cbc69c053ddc874d47e"}, + {file = "mypy-1.18.2.tar.gz", hash = "sha256:06a398102a5f203d7477b2923dda3634c36727fa5c237d8f859ef90c42a9924b"}, +] + +[package.dependencies] +mypy_extensions = ">=1.0.0" +pathspec = ">=0.9.0" +typing_extensions = ">=4.6.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + [[package]] name = "mypy-extensions" version = "1.1.0" @@ -2391,6 +2451,22 @@ sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-d test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"] xml = ["lxml (>=4.9.2)"] +[[package]] +name = "pandas-stubs" +version = "2.3.2.250926" +description = "Type annotations for pandas" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "pandas_stubs-2.3.2.250926-py3-none-any.whl", hash = "sha256:81121818453dcfe00f45c852f4dceee043640b813830f6e7bd084a4ef7ff7270"}, + {file = "pandas_stubs-2.3.2.250926.tar.gz", hash = "sha256:c64b9932760ceefb96a3222b953e6a251321a9832a28548be6506df473a66406"}, +] + +[package.dependencies] +numpy = ">=1.23.5" +types-pytz = ">=2022.1.1" + [[package]] name = "parsimonious" version = "0.10.0" @@ -3967,6 +4043,30 @@ files = [ [package.dependencies] typing_extensions = ">=4.14.0" +[[package]] +name = "types-pytz" +version = "2025.2.0.20250809" +description = "Typing stubs for pytz" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "types_pytz-2025.2.0.20250809-py3-none-any.whl", hash = "sha256:4f55ed1b43e925cf851a756fe1707e0f5deeb1976e15bf844bcaa025e8fbd0db"}, + {file = "types_pytz-2025.2.0.20250809.tar.gz", hash = "sha256:222e32e6a29bb28871f8834e8785e3801f2dc4441c715cd2082b271eecbe21e5"}, +] + +[[package]] +name = "types-pyyaml" +version = "6.0.12.20250915" +description = "Typing stubs for PyYAML" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "types_pyyaml-6.0.12.20250915-py3-none-any.whl", hash = "sha256:e7d4d9e064e89a3b3cae120b4990cd370874d2bf12fa5f46c97018dd5d3c9ab6"}, + {file = "types_pyyaml-6.0.12.20250915.tar.gz", hash = "sha256:0f8b54a528c303f0e6f7165687dd33fafa81c807fcac23f632b63aa624ced1d3"}, +] + [[package]] name = "typing-extensions" version = "4.15.0" @@ -4357,4 +4457,4 @@ propcache = ">=0.2.1" [metadata] lock-version = "2.1" python-versions = ">=3.11,<=3.13" -content-hash = "fa15635eb056cde0dfdfeb0a4f6dded7f9fda3a6975f2dbe95b98518624bc7ca" +content-hash = "4f3b8b0c9d420cf70f27704dbe6b760dc3ccab56920cbb38ba5a1930165ce00f" diff --git a/pyproject.toml b/pyproject.toml index ba1cc0ab..78ec6dd3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,7 @@ eth-typing = "<5" py-vollib = "^1.0.1" msgspec = "^0.19.0" + [tool.poetry.scripts] drv = "derive_client.cli:cli" fetch-drv-abis = "derive_client.utils.abi:download_prod_address_abis" @@ -37,7 +38,11 @@ ruff = "^0.13.0" datamodel-code-generator = "^0.34.0" libcst = "^1.8.5" pytest-asyncio = "<1.0.0" +mypy = "^1.18.2" pyright = "^1.1.407" +pydantic = { extras = ["mypy"], version = "^2.12.3" } +types-pyyaml = "^6.0.12.20250915" +pandas-stubs = "^2.3.2.250926" [tool.poetry.group.docs.dependencies] mkdocs = "^1.6.1" @@ -57,6 +62,18 @@ reportMissingImports = true reportMissingTypeStubs = false pythonVersion = "3.11" +[tool.mypy] +plugins = ["pydantic.mypy"] + +# [tool.mypy-derive_action_signing.module_data] +# ignore_missing_imports = true + + +[[tool.mypy.overrides]] +module = "derive_action_signing" +ignore_missing_imports = true + + [tool.ruff] line-length = 120 target-version = "py313" From 55006eed9982e91e836749e2b5c19358ccc0e235 Mon Sep 17 00:00:00 2001 From: zarathustra Date: Sun, 2 Nov 2025 18:02:55 +0100 Subject: [PATCH 20/22] tests: just updating tests as one does --- pyproject.toml | 5 ----- tests/test_clients/test_rest/test_async_http/test_account.py | 4 ++-- tests/test_clients/test_rest/test_async_http/test_api.py | 2 +- tests/test_clients/test_rest/test_async_http/test_markets.py | 2 +- tests/test_clients/test_rest/test_async_http/test_mmp.py | 2 +- tests/test_clients/test_rest/test_async_http/test_orders.py | 2 +- .../test_clients/test_rest/test_async_http/test_positions.py | 2 +- tests/test_clients/test_rest/test_async_http/test_rfq.py | 4 ++-- .../test_rest/test_async_http/test_transactions.py | 2 +- tests/test_clients/test_rest/test_http/test_account.py | 4 ++-- tests/test_clients/test_rest/test_http/test_api.py | 2 +- tests/test_clients/test_rest/test_http/test_markets.py | 2 +- tests/test_clients/test_rest/test_http/test_mmp.py | 2 +- tests/test_clients/test_rest/test_http/test_orders.py | 2 +- tests/test_clients/test_rest/test_http/test_positions.py | 2 +- tests/test_clients/test_rest/test_http/test_rfq.py | 5 +++-- tests/test_clients/test_rest/test_http/test_transactions.py | 2 +- tests/test_w3.py | 2 +- 18 files changed, 22 insertions(+), 26 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 78ec6dd3..2195a6fb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,15 +65,10 @@ pythonVersion = "3.11" [tool.mypy] plugins = ["pydantic.mypy"] -# [tool.mypy-derive_action_signing.module_data] -# ignore_missing_imports = true - - [[tool.mypy.overrides]] module = "derive_action_signing" ignore_missing_imports = true - [tool.ruff] line-length = 120 target-version = "py313" diff --git a/tests/test_clients/test_rest/test_async_http/test_account.py b/tests/test_clients/test_rest/test_async_http/test_account.py index ae49318c..6edfec09 100644 --- a/tests/test_clients/test_rest/test_async_http/test_account.py +++ b/tests/test_clients/test_rest/test_async_http/test_account.py @@ -3,8 +3,8 @@ import pytest from eth_account import Account -from derive_client.constants import INT64_MAX -from derive_client.data.generated.models import ( +from derive_client.config import INT64_MAX +from derive_client.data_types.generated_models import ( PrivateCreateSubaccountResultSchema, PrivateEditSessionKeyResultSchema, PrivateGetAccountResultSchema, diff --git a/tests/test_clients/test_rest/test_async_http/test_api.py b/tests/test_clients/test_rest/test_async_http/test_api.py index 92dbe716..9ee92bf2 100644 --- a/tests/test_clients/test_rest/test_async_http/test_api.py +++ b/tests/test_clients/test_rest/test_async_http/test_api.py @@ -2,7 +2,7 @@ import pytest -from derive_client.data.generated.models import ( +from derive_client.data_types.generated_models import ( PrivateGetOrdersParamsSchema, PrivateGetOrdersResponseSchema, PrivateGetSubaccountsParamsSchema, diff --git a/tests/test_clients/test_rest/test_async_http/test_markets.py b/tests/test_clients/test_rest/test_async_http/test_markets.py index af23b819..54f0cf0d 100644 --- a/tests/test_clients/test_rest/test_async_http/test_markets.py +++ b/tests/test_clients/test_rest/test_async_http/test_markets.py @@ -2,7 +2,7 @@ import pytest -from derive_client.data.generated.models import ( +from derive_client.data_types.generated_models import ( CurrencyDetailedResponseSchema, InstrumentPublicResponseSchema, InstrumentType, diff --git a/tests/test_clients/test_rest/test_async_http/test_mmp.py b/tests/test_clients/test_rest/test_async_http/test_mmp.py index 882f92eb..31e39896 100644 --- a/tests/test_clients/test_rest/test_async_http/test_mmp.py +++ b/tests/test_clients/test_rest/test_async_http/test_mmp.py @@ -2,7 +2,7 @@ import pytest -from derive_client.data.generated.models import ( +from derive_client.data_types.generated_models import ( MMPConfigResultSchema, PrivateSetMmpConfigResultSchema, Result, diff --git a/tests/test_clients/test_rest/test_async_http/test_orders.py b/tests/test_clients/test_rest/test_async_http/test_orders.py index 069c43f1..dec72821 100644 --- a/tests/test_clients/test_rest/test_async_http/test_orders.py +++ b/tests/test_clients/test_rest/test_async_http/test_orders.py @@ -4,7 +4,7 @@ import pytest -from derive_client.data.generated.models import ( +from derive_client.data_types.generated_models import ( Direction, OrderType, PrivateCancelByInstrumentResultSchema, diff --git a/tests/test_clients/test_rest/test_async_http/test_positions.py b/tests/test_clients/test_rest/test_async_http/test_positions.py index 29d25868..5a685d71 100644 --- a/tests/test_clients/test_rest/test_async_http/test_positions.py +++ b/tests/test_clients/test_rest/test_async_http/test_positions.py @@ -6,7 +6,7 @@ from derive_client._clients.rest.async_http.subaccount import Subaccount from derive_client._clients.utils import PositionTransfer -from derive_client.data.generated.models import ( +from derive_client.data_types.generated_models import ( Direction, PositionResponseSchema, PrivateTransferPositionResultSchema, diff --git a/tests/test_clients/test_rest/test_async_http/test_rfq.py b/tests/test_clients/test_rest/test_async_http/test_rfq.py index 88095556..0501e88f 100644 --- a/tests/test_clients/test_rest/test_async_http/test_rfq.py +++ b/tests/test_clients/test_rest/test_async_http/test_rfq.py @@ -5,8 +5,8 @@ import pytest -from derive_client.constants import INT64_MAX -from derive_client.data.generated.models import ( +from derive_client.config import INT64_MAX +from derive_client.data_types.generated_models import ( Direction, InstrumentType, LegPricedSchema, diff --git a/tests/test_clients/test_rest/test_async_http/test_transactions.py b/tests/test_clients/test_rest/test_async_http/test_transactions.py index 639a1baa..cd84cfad 100644 --- a/tests/test_clients/test_rest/test_async_http/test_transactions.py +++ b/tests/test_clients/test_rest/test_async_http/test_transactions.py @@ -4,7 +4,7 @@ import pytest -from derive_client.data.generated.models import ( +from derive_client.data_types.generated_models import ( PrivateDepositResultSchema, PrivateWithdrawResultSchema, PublicGetTransactionResultSchema, diff --git a/tests/test_clients/test_rest/test_http/test_account.py b/tests/test_clients/test_rest/test_http/test_account.py index 7d55d939..2c0a767b 100644 --- a/tests/test_clients/test_rest/test_http/test_account.py +++ b/tests/test_clients/test_rest/test_http/test_account.py @@ -3,8 +3,8 @@ import pytest from eth_account import Account -from derive_client.constants import INT64_MAX -from derive_client.data.generated.models import ( +from derive_client.config import INT64_MAX +from derive_client.data_types.generated_models import ( PrivateCreateSubaccountResultSchema, PrivateEditSessionKeyResultSchema, PrivateGetAccountResultSchema, diff --git a/tests/test_clients/test_rest/test_http/test_api.py b/tests/test_clients/test_rest/test_http/test_api.py index 053d1f9f..134feee1 100644 --- a/tests/test_clients/test_rest/test_http/test_api.py +++ b/tests/test_clients/test_rest/test_http/test_api.py @@ -1,6 +1,6 @@ """Tests for autogenerated API.""" -from derive_client.data.generated.models import ( +from derive_client.data_types.generated_models import ( PrivateGetOrdersParamsSchema, PrivateGetOrdersResponseSchema, PrivateGetSubaccountsParamsSchema, diff --git a/tests/test_clients/test_rest/test_http/test_markets.py b/tests/test_clients/test_rest/test_http/test_markets.py index 7d0e6d64..27c989fe 100644 --- a/tests/test_clients/test_rest/test_http/test_markets.py +++ b/tests/test_clients/test_rest/test_http/test_markets.py @@ -1,6 +1,6 @@ """Tests for Market module.""" -from derive_client.data.generated.models import ( +from derive_client.data_types.generated_models import ( CurrencyDetailedResponseSchema, InstrumentPublicResponseSchema, InstrumentType, diff --git a/tests/test_clients/test_rest/test_http/test_mmp.py b/tests/test_clients/test_rest/test_http/test_mmp.py index ddac628e..dec1cb24 100644 --- a/tests/test_clients/test_rest/test_http/test_mmp.py +++ b/tests/test_clients/test_rest/test_http/test_mmp.py @@ -1,6 +1,6 @@ """Tests for MMP module.""" -from derive_client.data.generated.models import ( +from derive_client.data_types.generated_models import ( MMPConfigResultSchema, PrivateSetMmpConfigResultSchema, Result, diff --git a/tests/test_clients/test_rest/test_http/test_orders.py b/tests/test_clients/test_rest/test_http/test_orders.py index a0bae006..e8c36a05 100644 --- a/tests/test_clients/test_rest/test_http/test_orders.py +++ b/tests/test_clients/test_rest/test_http/test_orders.py @@ -2,7 +2,7 @@ from decimal import Decimal -from derive_client.data.generated.models import ( +from derive_client.data_types.generated_models import ( Direction, OrderType, PrivateCancelByInstrumentResultSchema, diff --git a/tests/test_clients/test_rest/test_http/test_positions.py b/tests/test_clients/test_rest/test_http/test_positions.py index adc450e5..8b54eb21 100644 --- a/tests/test_clients/test_rest/test_http/test_positions.py +++ b/tests/test_clients/test_rest/test_http/test_positions.py @@ -4,7 +4,7 @@ from derive_client._clients.rest.http.subaccount import Subaccount from derive_client._clients.utils import PositionTransfer -from derive_client.data.generated.models import ( +from derive_client.data_types.generated_models import ( Direction, PositionResponseSchema, PrivateTransferPositionResultSchema, diff --git a/tests/test_clients/test_rest/test_http/test_rfq.py b/tests/test_clients/test_rest/test_http/test_rfq.py index d1a5b2ed..73c17b6d 100644 --- a/tests/test_clients/test_rest/test_http/test_rfq.py +++ b/tests/test_clients/test_rest/test_http/test_rfq.py @@ -3,8 +3,7 @@ import time from decimal import Decimal -from derive_client.constants import INT64_MAX -from derive_client.data.generated.models import ( +from derive_client.data_types.generated_models import ( Direction, InstrumentType, LegPricedSchema, @@ -21,6 +20,8 @@ PrivateSendRfqResultSchema, Result, ) + +from derive_client.config import INT64_MAX from tests.conftest import assert_api_calls diff --git a/tests/test_clients/test_rest/test_http/test_transactions.py b/tests/test_clients/test_rest/test_http/test_transactions.py index ea345f0f..529fc7a9 100644 --- a/tests/test_clients/test_rest/test_http/test_transactions.py +++ b/tests/test_clients/test_rest/test_http/test_transactions.py @@ -2,7 +2,7 @@ from decimal import Decimal -from derive_client.data.generated.models import ( +from derive_client.data_types.generated_models import ( PrivateDepositResultSchema, PrivateWithdrawResultSchema, PublicGetTransactionResultSchema, diff --git a/tests/test_w3.py b/tests/test_w3.py index 4cfe789d..620319b2 100644 --- a/tests/test_w3.py +++ b/tests/test_w3.py @@ -9,7 +9,7 @@ from web3.exceptions import MethodUnavailable from web3.providers import HTTPProvider -from derive_client.constants import DEFAULT_RPC_ENDPOINTS +from derive_client.config import DEFAULT_RPC_ENDPOINTS from derive_client.data_types import ChainID, EthereumJSONRPCErrorCode from derive_client.utils import get_logger, load_rpc_endpoints from derive_client.utils.w3 import make_rotating_provider_middleware From 90d377e05115d489c5694e3317f41c2e8bd62327 Mon Sep 17 00:00:00 2001 From: zarathustra Date: Mon, 3 Nov 2025 12:08:45 +0100 Subject: [PATCH 21/22] chore: make fmt lint --- derive_client/_bridge/_derive_bridge.py | 4 +- derive_client/_bridge/w3.py | 5 +- .../_clients/rest/async_http/account.py | 4 +- derive_client/_clients/rest/async_http/api.py | 3 +- .../_clients/rest/async_http/markets.py | 12 ++- .../_clients/rest/async_http/subaccount.py | 3 +- derive_client/_clients/rest/http/account.py | 4 +- derive_client/_clients/rest/http/api.py | 3 +- .../_clients/rest/http/subaccount.py | 3 +- derive_client/_clients/utils.py | 3 +- derive_client/config/__init__.py | 102 ++++++++++++++++-- derive_client/config/constants.py | 6 -- derive_client/config/contracts.py | 54 ++-------- derive_client/config/networks.py | 12 --- derive_client/data_types/__init__.py | 4 + derive_client/data_types/models.py | 32 +++++- derive_client/utils/w3.py | 20 ++-- examples/cancel_orders.py | 2 +- examples/create_order.py | 2 +- examples/deposit_to_derive.py | 2 +- examples/fetch_instruments.py | 2 +- examples/get_subaccounts.py | 2 +- examples/transfer_position.py | 2 +- examples/transfer_positions.py | 2 +- examples/websockets/spot_stable_quoter.py | 6 +- examples/websockets/websocket_quoter.py | 6 +- examples/withdraw_from_derive.py | 2 +- .../test_rest/test_http/test_rfq.py | 3 +- 28 files changed, 191 insertions(+), 114 deletions(-) diff --git a/derive_client/_bridge/_derive_bridge.py b/derive_client/_bridge/_derive_bridge.py index f897ba03..347e8080 100644 --- a/derive_client/_bridge/_derive_bridge.py +++ b/derive_client/_bridge/_derive_bridge.py @@ -27,8 +27,8 @@ ERC20_ABI_PATH, ETH_DEPOSIT_WRAPPER, LIGHT_ACCOUNT_ABI_PATH, + LYRA_OFT_WITHDRAW_WRAPPER, LYRA_OFT_WITHDRAW_WRAPPER_ABI_PATH, - LYRA_OFT_WITHDRAW_WRAPPER_ADDRESS, MSG_GAS_LIMIT, NEW_VAULT_ABI_PATH, OLD_VAULT_ABI_PATH, @@ -504,7 +504,7 @@ async def _prepare_layerzero_deposit(self, amount: int, context: BridgeContext) async def _prepare_layerzero_withdrawal(self, amount: int, context: BridgeContext) -> PreparedBridgeTx: abi = json.loads(LYRA_OFT_WITHDRAW_WRAPPER_ABI_PATH.read_text()) - withdraw_wrapper = get_contract(context.source_w3, LYRA_OFT_WITHDRAW_WRAPPER_ADDRESS, abi=abi) + withdraw_wrapper = get_contract(context.source_w3, LYRA_OFT_WITHDRAW_WRAPPER, abi=abi) destEID = LayerZeroChainIDv2[context.target_chain.name] fee_in_token = await withdraw_wrapper.functions.getFeeInToken( diff --git a/derive_client/_bridge/w3.py b/derive_client/_bridge/w3.py index 663b1972..b59d5801 100644 --- a/derive_client/_bridge/w3.py +++ b/derive_client/_bridge/w3.py @@ -13,7 +13,7 @@ from requests import RequestException from web3 import AsyncHTTPProvider, AsyncWeb3 from web3.contract.async_contract import AsyncContract, AsyncContractEvent, AsyncContractFunction -from web3.types import RPCEndpoint, RPCError, RPCResponse +from web3.types import RPCEndpoint, RPCResponse from derive_client.config import ( ABI_DATA_DIR, @@ -94,7 +94,8 @@ async def rotating_backoff(method: RPCEndpoint, params: Any) -> RPCResponse: try: # 3) attempt the request - resp = await state.provider.make_request(method, params) + provider = cast(AsyncHTTPProvider, state.provider) + resp = await provider.make_request(method, params) # Json‑RPC error branch if isinstance(resp, dict) and (error := resp.get("error")): diff --git a/derive_client/_clients/rest/async_http/account.py b/derive_client/_clients/rest/async_http/account.py index ae86ab46..a506698d 100644 --- a/derive_client/_clients/rest/async_http/account.py +++ b/derive_client/_clients/rest/async_http/account.py @@ -10,8 +10,8 @@ from derive_client._clients.rest.async_http.api import AsyncPrivateAPI, AsyncPublicAPI from derive_client._clients.utils import AuthContext -from derive_client.config import CURRENCY_DECIMALS, Currency, EnvConfig -from derive_client.data_types import ChecksumAddress +from derive_client.config import CURRENCY_DECIMALS +from derive_client.data_types import ChecksumAddress, Currency, EnvConfig from derive_client.data_types.generated_models import ( MarginType, PrivateCreateSubaccountParamsSchema, diff --git a/derive_client/_clients/rest/async_http/api.py b/derive_client/_clients/rest/async_http/api.py index acca2a9e..0ccc63ff 100644 --- a/derive_client/_clients/rest/async_http/api.py +++ b/derive_client/_clients/rest/async_http/api.py @@ -3,7 +3,8 @@ from derive_client._clients.rest.async_http.session import AsyncHTTPSession from derive_client._clients.rest.endpoints import PrivateEndpoints, PublicEndpoints from derive_client._clients.utils import AuthContext, encode_json_exclude_none, try_cast_response -from derive_client.config import PUBLIC_HEADERS, EnvConfig +from derive_client.config import PUBLIC_HEADERS +from derive_client.data_types import EnvConfig from derive_client.data_types.generated_models import ( PrivateCancelAllParamsSchema, PrivateCancelAllResponseSchema, diff --git a/derive_client/_clients/rest/async_http/markets.py b/derive_client/_clients/rest/async_http/markets.py index b2008933..f0542b3b 100644 --- a/derive_client/_clients/rest/async_http/markets.py +++ b/derive_client/_clients/rest/async_http/markets.py @@ -46,7 +46,9 @@ def erc20_instruments_cache(self) -> dict[str, InstrumentPublicResponseSchema]: """Get cached ERC20 instruments.""" if not self._erc20_instruments_cache: - raise RuntimeError("Call fetch_instruments() or fetch_all_instruments() to create the erc20_instruments_cache.") + raise RuntimeError( + "Call fetch_instruments() or fetch_all_instruments() to create the erc20_instruments_cache." + ) return self._erc20_instruments_cache @property @@ -54,7 +56,9 @@ def perp_instruments_cache(self) -> dict[str, InstrumentPublicResponseSchema]: """Get cached perpetual instruments.""" if not self._perp_instruments_cache: - raise RuntimeError("Call fetch_instruments() or fetch_all_instruments() to create the perp_instruments_cache.") + raise RuntimeError( + "Call fetch_instruments() or fetch_all_instruments() to create the perp_instruments_cache." + ) return self._perp_instruments_cache @property @@ -62,7 +66,9 @@ def option_instruments_cache(self) -> dict[str, InstrumentPublicResponseSchema]: """Get cached option instruments.""" if not self._option_instruments_cache: - raise RuntimeError("Call fetch_instruments() or fetch_all_instruments() to create the option_instruments_cache.") + raise RuntimeError( + "Call fetch_instruments() or fetch_all_instruments() to create the option_instruments_cache." + ) return self._option_instruments_cache async def fetch_instruments( diff --git a/derive_client/_clients/rest/async_http/subaccount.py b/derive_client/_clients/rest/async_http/subaccount.py index 26993447..35eac219 100644 --- a/derive_client/_clients/rest/async_http/subaccount.py +++ b/derive_client/_clients/rest/async_http/subaccount.py @@ -16,8 +16,7 @@ from derive_client._clients.rest.async_http.rfq import RFQOperations from derive_client._clients.rest.async_http.transactions import TransactionOperations from derive_client._clients.utils import AuthContext -from derive_client.config import EnvConfig -from derive_client.data_types import ChecksumAddress +from derive_client.data_types import ChecksumAddress, EnvConfig from derive_client.data_types.generated_models import ( MarginType, PrivateGetSubaccountParamsSchema, diff --git a/derive_client/_clients/rest/http/account.py b/derive_client/_clients/rest/http/account.py index e8e1ae59..6dc49daa 100644 --- a/derive_client/_clients/rest/http/account.py +++ b/derive_client/_clients/rest/http/account.py @@ -10,8 +10,8 @@ from derive_client._clients.rest.http.api import PrivateAPI, PublicAPI from derive_client._clients.utils import AuthContext -from derive_client.config import CURRENCY_DECIMALS, Currency, EnvConfig -from derive_client.data_types import ChecksumAddress +from derive_client.config import CURRENCY_DECIMALS +from derive_client.data_types import ChecksumAddress, Currency, EnvConfig from derive_client.data_types.generated_models import ( MarginType, PrivateCreateSubaccountParamsSchema, diff --git a/derive_client/_clients/rest/http/api.py b/derive_client/_clients/rest/http/api.py index 3c329692..73f51c23 100644 --- a/derive_client/_clients/rest/http/api.py +++ b/derive_client/_clients/rest/http/api.py @@ -3,7 +3,8 @@ from derive_client._clients.rest.endpoints import PrivateEndpoints, PublicEndpoints from derive_client._clients.rest.http.session import HTTPSession from derive_client._clients.utils import AuthContext, encode_json_exclude_none, try_cast_response -from derive_client.config import PUBLIC_HEADERS, EnvConfig +from derive_client.config import PUBLIC_HEADERS +from derive_client.data_types import EnvConfig from derive_client.data_types.generated_models import ( PrivateCancelAllParamsSchema, PrivateCancelAllResponseSchema, diff --git a/derive_client/_clients/rest/http/subaccount.py b/derive_client/_clients/rest/http/subaccount.py index eb2da9f6..0131018b 100644 --- a/derive_client/_clients/rest/http/subaccount.py +++ b/derive_client/_clients/rest/http/subaccount.py @@ -16,8 +16,7 @@ from derive_client._clients.rest.http.rfq import RFQOperations from derive_client._clients.rest.http.transactions import TransactionOperations from derive_client._clients.utils import AuthContext -from derive_client.config import EnvConfig -from derive_client.data_types import ChecksumAddress +from derive_client.data_types import ChecksumAddress, EnvConfig from derive_client.data_types.generated_models import ( MarginType, PrivateGetSubaccountParamsSchema, diff --git a/derive_client/_clients/utils.py b/derive_client/_clients/utils.py index 0e2856b7..5fd2e1b3 100644 --- a/derive_client/_clients/utils.py +++ b/derive_client/_clients/utils.py @@ -13,8 +13,7 @@ from pydantic import BaseModel from web3 import AsyncWeb3, Web3 -from derive_client.config import EnvConfig -from derive_client.data_types import ChecksumAddress, PositionTransfer +from derive_client.data_types import ChecksumAddress, EnvConfig, PositionTransfer from derive_client.data_types.generated_models import ( InstrumentPublicResponseSchema, InstrumentType, diff --git a/derive_client/config/__init__.py b/derive_client/config/__init__.py index 07065eac..16ebbdb0 100644 --- a/derive_client/config/__init__.py +++ b/derive_client/config/__init__.py @@ -2,10 +2,7 @@ ABI_DATA_DIR, ASSUMED_BRIDGE_GAS_LIMIT, DATA_DIR, - DEFAULT_GAS_FUNDING_AMOUNT, - DEFAULT_REFERER, DEFAULT_RPC_ENDPOINTS, - DEFAULT_SPOT_QUOTE_TOKEN, GAS_FEE_BUFFER, GAS_LIMIT_BUFFER, INT32_MAX, @@ -16,9 +13,102 @@ PKG_ROOT, PUBLIC_HEADERS, TARGET_SPEED, - TEST_PRIVATE_KEY, UINT32_MAX, UINT64_MAX, ) -from .contracts import * -from .networks import * +from .contracts import ( + ARBITRUM_DEPOSIT_WRAPPER, + BASE_DEPOSIT_WRAPPER, + CONFIGS, + CONNECTOR_PLUG, + CONTROLLER_ABI_PATH, + CONTROLLER_V0_ABI_PATH, + DEPOSIT_HELPER_ABI_PATH, + DEPOSIT_HOOK_ABI_PATH, + DERIVE_ABI_PATH, + DERIVE_L2_ABI_PATH, + ERC20_ABI_PATH, + ETH_DEPOSIT_WRAPPER, + L1_CHUG_SPLASH_PROXY, + L1_CHUG_SPLASH_PROXY_ABI_PATH, + L1_CROSS_DOMAIN_MESSENGER_ABI_PATH, + L1_STANDARD_BRIDGE_ABI_PATH, + L2_CROSS_DOMAIN_MESSENGER_ABI_PATH, + L2_CROSS_DOMAIN_MESSENGER_PROXY, + L2_STANDARD_BRIDGE_ABI_PATH, + L2_STANDARD_BRIDGE_PROXY, + LIGHT_ACCOUNT_ABI_PATH, + LYRA_OFT_WITHDRAW_WRAPPER, + LYRA_OFT_WITHDRAW_WRAPPER_ABI_PATH, + NEW_VAULT_ABI_PATH, + OLD_VAULT_ABI_PATH, + OPTIMISM_DEPOSIT_WRAPPER, + RESOLVED_DELEGATE_PROXY, + SOCKET_ABI_PATH, + WITHDRAW_WRAPPER_V2, + WITHDRAW_WRAPPER_V2_ABI_PATH, +) +from .networks import ( + CURRENCY_DECIMALS, + TOKEN_DECIMALS, + DeriveTokenAddress, + LayerZeroChainIDv2, + SocketAddress, +) + +__all__ = [ + # constants + "ABI_DATA_DIR", + "ASSUMED_BRIDGE_GAS_LIMIT", + "DATA_DIR", + "DEFAULT_RPC_ENDPOINTS", + "GAS_FEE_BUFFER", + "GAS_LIMIT_BUFFER", + "INT32_MAX", + "INT64_MAX", + "MIN_PRIORITY_FEE", + "MSG_GAS_LIMIT", + "PAYLOAD_SIZE", + "PKG_ROOT", + "PUBLIC_HEADERS", + "TARGET_SPEED", + "UINT32_MAX", + "UINT64_MAX", + # contracts + "ARBITRUM_DEPOSIT_WRAPPER", + "BASE_DEPOSIT_WRAPPER", + "CONFIGS", + "CONNECTOR_PLUG", + "CONTROLLER_ABI_PATH", + "CONTROLLER_V0_ABI_PATH", + "DEPOSIT_HELPER_ABI_PATH", + "DEPOSIT_HOOK_ABI_PATH", + "DERIVE_ABI_PATH", + "DERIVE_L2_ABI_PATH", + "ERC20_ABI_PATH", + "ETH_DEPOSIT_WRAPPER", + "L1_CHUG_SPLASH_PROXY", + "L1_CHUG_SPLASH_PROXY_ABI_PATH", + "L1_CROSS_DOMAIN_MESSENGER_ABI_PATH", + "L1_STANDARD_BRIDGE_ABI_PATH", + "L2_CROSS_DOMAIN_MESSENGER_ABI_PATH", + "L2_CROSS_DOMAIN_MESSENGER_PROXY", + "L2_STANDARD_BRIDGE_ABI_PATH", + "L2_STANDARD_BRIDGE_PROXY", + "LIGHT_ACCOUNT_ABI_PATH", + "LYRA_OFT_WITHDRAW_WRAPPER", + "LYRA_OFT_WITHDRAW_WRAPPER_ABI_PATH", + "NEW_VAULT_ABI_PATH", + "OLD_VAULT_ABI_PATH", + "OPTIMISM_DEPOSIT_WRAPPER", + "RESOLVED_DELEGATE_PROXY", + "SOCKET_ABI_PATH", + "WITHDRAW_WRAPPER_V2", + "WITHDRAW_WRAPPER_V2_ABI_PATH", + # networks + "CURRENCY_DECIMALS", + "TOKEN_DECIMALS", + "DeriveTokenAddress", + "LayerZeroChainIDv2", + "SocketAddress", +] diff --git a/derive_client/config/constants.py b/derive_client/config/constants.py index c7ca3312..d34b3eea 100644 --- a/derive_client/config/constants.py +++ b/derive_client/config/constants.py @@ -15,10 +15,6 @@ PUBLIC_HEADERS = {"accept": "application/json", "content-type": "application/json"} -TEST_PRIVATE_KEY = "0xc14f53ee466dd3fc5fa356897ab276acbef4f020486ec253a23b0d1c3f89d4f4" -DEFAULT_SPOT_QUOTE_TOKEN = "USDC" - -DEFAULT_REFERER = "0x9135BA0f495244dc0A5F029b25CDE95157Db89AD" GAS_FEE_BUFFER = 1.1 # buffer multiplier to pad maxFeePerGas GAS_LIMIT_BUFFER = 1.1 # buffer multiplier to pad gas limit @@ -28,6 +24,4 @@ PAYLOAD_SIZE = 161 TARGET_SPEED = "FAST" -DEFAULT_GAS_FUNDING_AMOUNT = int(0.0001 * 1e18) # 0.0001 ETH - DEFAULT_RPC_ENDPOINTS = DATA_DIR / "rpc_endpoints.yaml" diff --git a/derive_client/config/contracts.py b/derive_client/config/contracts.py index 4ddfdd9c..f5071c62 100644 --- a/derive_client/config/contracts.py +++ b/derive_client/config/contracts.py @@ -1,51 +1,9 @@ """Contract addresses and environment configurations.""" -from pydantic import BaseModel - -from derive_client.data_types import ChecksumAddress, Environment +from derive_client.data_types import ChecksumAddress, DeriveContractAddresses, EnvConfig, Environment from .constants import ABI_DATA_DIR - -class ContractAddresses(BaseModel, frozen=True): - ETH_PERP: ChecksumAddress - BTC_PERP: ChecksumAddress - ETH_OPTION: ChecksumAddress - BTC_OPTION: ChecksumAddress - TRADE_MODULE: ChecksumAddress - RFQ_MODULE: ChecksumAddress - STANDARD_RISK_MANAGER: ChecksumAddress - BTC_PORTFOLIO_RISK_MANAGER: ChecksumAddress - ETH_PORTFOLIO_RISK_MANAGER: ChecksumAddress - CASH_ASSET: ChecksumAddress - USDC_ASSET: ChecksumAddress - DEPOSIT_MODULE: ChecksumAddress - WITHDRAWAL_MODULE: ChecksumAddress - TRANSFER_MODULE: ChecksumAddress - - def __getitem__(self, key): - return getattr(self, key) - - -class EnvConfig(BaseModel, frozen=True): - base_url: str - ws_address: str - rpc_endpoint: str - block_explorer: str - ACTION_TYPEHASH: str - DOMAIN_SEPARATOR: str - contracts: ContractAddresses - - -# PKG_ROOT = Path(__file__).parent.parent -# DATA_DIR = PKG_ROOT / "data" -# ABI_DATA_DIR = DATA_DIR / "abi" - -PUBLIC_HEADERS = {"accept": "application/json", "content-type": "application/json"} - -TEST_PRIVATE_KEY = "0xc14f53ee466dd3fc5fa356897ab276acbef4f020486ec253a23b0d1c3f89d4f4" -DEFAULT_SPOT_QUOTE_TOKEN = "USDC" - CONFIGS: dict[Environment, EnvConfig] = { Environment.TEST: EnvConfig( base_url="https://api-demo.lyra.finance", @@ -54,7 +12,7 @@ class EnvConfig(BaseModel, frozen=True): block_explorer="https://explorer-prod-testnet-0eakp60405.t.conduit.xyz", ACTION_TYPEHASH="0x4d7a9f27c403ff9c0f19bce61d76d82f9aa29f8d6d4b0c5474607d9770d1af17", DOMAIN_SEPARATOR="0x9bcf4dc06df5d8bf23af818d5716491b995020f377d3b7b64c29ed14e3dd1105", - contracts=ContractAddresses( + contracts=DeriveContractAddresses( ETH_PERP="0x010e26422790C6Cb3872330980FAa7628FD20294", BTC_PERP="0xAFB6Bb95cd70D5367e2C39e9dbEb422B9815339D", ETH_OPTION="0xBcB494059969DAaB460E0B5d4f5c2366aab79aa1", @@ -78,7 +36,7 @@ class EnvConfig(BaseModel, frozen=True): block_explorer="https://explorer.lyra.finance", ACTION_TYPEHASH="0x4d7a9f27c403ff9c0f19bce61d76d82f9aa29f8d6d4b0c5474607d9770d1af17", DOMAIN_SEPARATOR="0xd96e5f90797da7ec8dc4e276260c7f3f87fedf68775fbe1ef116e996fc60441b", - contracts=ContractAddresses( + contracts=DeriveContractAddresses( ETH_PERP="0xAf65752C4643E25C02F693f9D4FE19cF23a095E3", BTC_PERP="0xDBa83C0C654DB1cd914FA2710bA743e925B53086", ETH_OPTION="0x4BB4C3CDc7562f08e9910A0C7D8bB7e108861eB4", @@ -119,8 +77,10 @@ class EnvConfig(BaseModel, frozen=True): CONNECTOR_PLUG = ABI_DATA_DIR / "ConnectorPlug.json" -# Contracts used in bridging module -LYRA_OFT_WITHDRAW_WRAPPER_ADDRESS = ChecksumAddress("0x9400cc156dad38a716047a67c897973A29A06710") +# =========================== +# Bridge Contract Addresses +# =========================== +LYRA_OFT_WITHDRAW_WRAPPER = ChecksumAddress("0x9400cc156dad38a716047a67c897973A29A06710") L1_CHUG_SPLASH_PROXY = ChecksumAddress("0x61e44dc0dae6888b5a301887732217d5725b0bff") RESOLVED_DELEGATE_PROXY = ChecksumAddress("0x5456f02c08e9A018E42C39b351328E5AA864174A") L2_STANDARD_BRIDGE_PROXY = ChecksumAddress("0x4200000000000000000000000000000000000010") diff --git a/derive_client/config/networks.py b/derive_client/config/networks.py index 3c8210d5..147360e4 100644 --- a/derive_client/config/networks.py +++ b/derive_client/config/networks.py @@ -41,18 +41,6 @@ class DeriveTokenAddress(Enum): DERIVE = ChecksumAddress("0x2EE0fd70756EDC663AcC9676658A1497C247693A") -DEFAULT_REFERER = "0x9135BA0f495244dc0A5F029b25CDE95157Db89AD" - -GAS_FEE_BUFFER = 1.1 # buffer multiplier to pad maxFeePerGas -GAS_LIMIT_BUFFER = 1.1 # buffer multiplier to pad gas limit -MSG_GAS_LIMIT = 200_000 -ASSUMED_BRIDGE_GAS_LIMIT = 1_000_000 -MIN_PRIORITY_FEE = 10_000 -PAYLOAD_SIZE = 161 -TARGET_SPEED = "FAST" - -DEFAULT_GAS_FUNDING_AMOUNT = int(0.0001 * 1e18) # 0.0001 ETH - TOKEN_DECIMALS = { UnderlyingCurrency.ETH: 18, UnderlyingCurrency.BTC: 8, diff --git a/derive_client/data_types/__init__.py b/derive_client/data_types/__init__.py index cc92c097..9a826425 100644 --- a/derive_client/data_types/__init__.py +++ b/derive_client/data_types/__init__.py @@ -23,6 +23,8 @@ BridgeTxResult, ChecksumAddress, DeriveAddresses, + DeriveContractAddresses, + EnvConfig, FeeEstimate, FeeEstimates, FeeHistory, @@ -50,11 +52,13 @@ "BridgeType", "BridgeContext", "BridgeTxResult", + "EnvConfig", "TxResult", "Currency", "InstrumentType", "EthereumJSONRPCErrorCode", "DeriveJSONRPCErrorCode", + "DeriveContractAddresses", "UnderlyingCurrency", "OrderType", "Environment", diff --git a/derive_client/data_types/models.py b/derive_client/data_types/models.py index 80a28026..51be6131 100644 --- a/derive_client/data_types/models.py +++ b/derive_client/data_types/models.py @@ -4,7 +4,7 @@ from dataclasses import dataclass from decimal import Decimal -from typing import TYPE_CHECKING, Any, Literal, cast +from typing import Any, Literal, cast from eth_account.datastructures import SignedTransaction from eth_typing import BlockNumber, HexStr @@ -38,6 +38,36 @@ ) +class DeriveContractAddresses(BaseModel, frozen=True): + ETH_PERP: ChecksumAddress + BTC_PERP: ChecksumAddress + ETH_OPTION: ChecksumAddress + BTC_OPTION: ChecksumAddress + TRADE_MODULE: ChecksumAddress + RFQ_MODULE: ChecksumAddress + STANDARD_RISK_MANAGER: ChecksumAddress + BTC_PORTFOLIO_RISK_MANAGER: ChecksumAddress + ETH_PORTFOLIO_RISK_MANAGER: ChecksumAddress + CASH_ASSET: ChecksumAddress + USDC_ASSET: ChecksumAddress + DEPOSIT_MODULE: ChecksumAddress + WITHDRAWAL_MODULE: ChecksumAddress + TRANSFER_MODULE: ChecksumAddress + + def __getitem__(self, key): + return getattr(self, key) + + +class EnvConfig(BaseModel, frozen=True): + base_url: str + ws_address: str + rpc_endpoint: str + block_explorer: str + ACTION_TYPEHASH: str + DOMAIN_SEPARATOR: str + contracts: DeriveContractAddresses + + class PHexBytes(HexBytes): @classmethod def __get_pydantic_core_schema__(cls, _source: Any, _handler: Any) -> core_schema.CoreSchema: diff --git a/derive_client/utils/w3.py b/derive_client/utils/w3.py index 85f229e9..c6fe1ac5 100644 --- a/derive_client/utils/w3.py +++ b/derive_client/utils/w3.py @@ -9,7 +9,7 @@ import yaml from requests import RequestException -from web3 import AsyncHTTPProvider, Web3 +from web3 import AsyncHTTPProvider, HTTPProvider, Web3 from web3.types import RPCEndpoint, RPCResponse from derive_client.config import CURRENCY_DECIMALS, DEFAULT_RPC_ENDPOINTS @@ -21,7 +21,7 @@ class EndpointState: __slots__ = ("provider", "backoff", "next_available") - def __init__(self, provider: AsyncHTTPProvider): + def __init__(self, provider: HTTPProvider | AsyncHTTPProvider): self.provider = provider self.backoff = 0.0 self.next_available = 0.0 @@ -34,7 +34,7 @@ def __str__(self) -> str: def make_rotating_provider_middleware( - endpoints: list[AsyncHTTPProvider], + endpoints: list[HTTPProvider], *, initial_backoff: float = 1.0, max_backoff: float = 600.0, @@ -81,9 +81,15 @@ def rotating_backoff(method: RPCEndpoint, params: Any) -> Any: state.next_available = now + state.backoff with lock: heapq.heappush(heap, state) - err_msg = error.get("message", "") - msg = "RPC error on %s: %s → backing off %.2fs" - logger.info(msg, state.provider.endpoint_uri, err_msg, state.backoff, extra=resp) + + if isinstance(error, str): + msg = error + else: + err_msg = error.get("message", "") + err_code = error.get("code", "") + msg = "RPC error on %s: %s (code: %s)→ backing off %.2fs" + + logger.info(msg, state.provider.endpoint_uri, err_msg, err_code, state.backoff) continue # 4) on success, reset its backoff and re-schedule immediately @@ -143,7 +149,7 @@ def get_w3_connection( logger: Logger | None = None, ) -> Web3: rpc_endpoints = rpc_endpoints or load_rpc_endpoints(DEFAULT_RPC_ENDPOINTS) - providers = [AsyncHTTPProvider(str(url)) for url in rpc_endpoints[chain_id]] + providers = [HTTPProvider(str(url)) for url in rpc_endpoints[chain_id]] logger = logger or get_logger() diff --git a/examples/cancel_orders.py b/examples/cancel_orders.py index 964b0bc1..2bd6b51b 100644 --- a/examples/cancel_orders.py +++ b/examples/cancel_orders.py @@ -2,10 +2,10 @@ Sample for cancelling an order on the derive client. """ +from derive_client.derive import DeriveClient from rich import print from derive_client.data_types import Environment -from derive_client.derive import DeriveClient from tests.conftest import TEST_PRIVATE_KEY, TEST_WALLET diff --git a/examples/create_order.py b/examples/create_order.py index 92c0a0c6..54b91051 100644 --- a/examples/create_order.py +++ b/examples/create_order.py @@ -2,10 +2,10 @@ Sample for creating an order on the derive client. """ +from derive_client.derive import DeriveClient from rich import print from derive_client.data_types import Environment, InstrumentType, OrderSide, UnderlyingCurrency -from derive_client.derive import DeriveClient from tests.conftest import TEST_PRIVATE_KEY, TEST_WALLET diff --git a/examples/deposit_to_derive.py b/examples/deposit_to_derive.py index bfbe1653..593e1bb6 100644 --- a/examples/deposit_to_derive.py +++ b/examples/deposit_to_derive.py @@ -5,10 +5,10 @@ import os import click +from derive_client.derive import DeriveClient from dotenv import load_dotenv from derive_client.data_types import Address, ChainID, Currency, Environment -from derive_client.derive import DeriveClient ChainChoice = click.Choice([c.name for c in ChainID]) CurrencyChoice = click.Choice([c.name for c in Currency]) diff --git a/examples/fetch_instruments.py b/examples/fetch_instruments.py index c1be0e5b..c08d8769 100644 --- a/examples/fetch_instruments.py +++ b/examples/fetch_instruments.py @@ -2,10 +2,10 @@ Sample of fetching instruments from the derive client, and printing the result. """ +from derive_client.derive import DeriveClient from rich import print from derive_client.data_types import Environment, InstrumentType, UnderlyingCurrency -from derive_client.derive import DeriveClient from tests.conftest import TEST_PRIVATE_KEY, TEST_WALLET diff --git a/examples/get_subaccounts.py b/examples/get_subaccounts.py index 0cd34da5..89a7a272 100644 --- a/examples/get_subaccounts.py +++ b/examples/get_subaccounts.py @@ -7,8 +7,8 @@ from pathlib import Path import click - from derive_client.clients import HttpClient as DeriveClient + from derive_client.data_types import Environment diff --git a/examples/transfer_position.py b/examples/transfer_position.py index a545d94a..83475562 100644 --- a/examples/transfer_position.py +++ b/examples/transfer_position.py @@ -7,10 +7,10 @@ import time +from derive_client.derive import DeriveClient from rich import print from derive_client.data_types import Environment, InstrumentType, OrderSide, OrderType, UnderlyingCurrency -from derive_client.derive import DeriveClient # Configuration - update these values for your setup WALLET = "0xA419f70C696a4b449a4A24F92e955D91482d44e9" diff --git a/examples/transfer_positions.py b/examples/transfer_positions.py index d5915d33..9044156c 100644 --- a/examples/transfer_positions.py +++ b/examples/transfer_positions.py @@ -7,6 +7,7 @@ import time +from derive_client.derive import DeriveClient from rich import print from derive_client.data_types import ( @@ -19,7 +20,6 @@ TransferPosition, UnderlyingCurrency, ) -from derive_client.derive import DeriveClient # Configuration - update these values for your setup WALLET = "0xA419f70C696a4b449a4A24F92e955D91482d44e9" diff --git a/examples/websockets/spot_stable_quoter.py b/examples/websockets/spot_stable_quoter.py index 041f19d7..c705a309 100644 --- a/examples/websockets/spot_stable_quoter.py +++ b/examples/websockets/spot_stable_quoter.py @@ -4,9 +4,6 @@ import os -from dotenv import load_dotenv -from websockets import ConnectionClosedError - from derive_client.clients.ws_client import ( Orderbook, OrderResponseSchema, @@ -15,6 +12,9 @@ WsClient, ) from derive_client.data.generated.models import Direction, OrderStatus, PublicGetTickerResultSchema +from dotenv import load_dotenv +from websockets import ConnectionClosedError + from derive_client.data_types import Environment from derive_client.data_types.enums import OrderSide, OrderType diff --git a/examples/websockets/websocket_quoter.py b/examples/websockets/websocket_quoter.py index 39ab8765..57bb2bc5 100644 --- a/examples/websockets/websocket_quoter.py +++ b/examples/websockets/websocket_quoter.py @@ -4,9 +4,6 @@ import os -from dotenv import load_dotenv -from websockets import ConnectionClosedError - from derive_client.clients.ws_client import ( Orderbook, OrderResponseSchema, @@ -17,6 +14,9 @@ WsClient, ) from derive_client.data.generated.models import Direction, OrderStatus +from dotenv import load_dotenv +from websockets import ConnectionClosedError + from derive_client.data_types import Environment from derive_client.data_types.enums import OrderSide, OrderType diff --git a/examples/withdraw_from_derive.py b/examples/withdraw_from_derive.py index e3621392..d941bf14 100644 --- a/examples/withdraw_from_derive.py +++ b/examples/withdraw_from_derive.py @@ -5,10 +5,10 @@ import os import click +from derive_client.derive import DeriveClient from dotenv import load_dotenv from derive_client.data_types import Address, ChainID, Currency, Environment -from derive_client.derive import DeriveClient ChainChoice = click.Choice([c.name for c in ChainID]) CurrencyChoice = click.Choice([c.name for c in Currency]) diff --git a/tests/test_clients/test_rest/test_http/test_rfq.py b/tests/test_clients/test_rest/test_http/test_rfq.py index 73c17b6d..b080ac56 100644 --- a/tests/test_clients/test_rest/test_http/test_rfq.py +++ b/tests/test_clients/test_rest/test_http/test_rfq.py @@ -3,6 +3,7 @@ import time from decimal import Decimal +from derive_client.config import INT64_MAX from derive_client.data_types.generated_models import ( Direction, InstrumentType, @@ -20,8 +21,6 @@ PrivateSendRfqResultSchema, Result, ) - -from derive_client.config import INT64_MAX from tests.conftest import assert_api_calls From 15bbba1f8310506966a90648810a2806bb2c3db4 Mon Sep 17 00:00:00 2001 From: zarathustra Date: Mon, 3 Nov 2025 12:09:31 +0100 Subject: [PATCH 22/22] feat: scripts/download-prod-abis.py --- derive_client/data/templates/api.py.jinja | 3 +- scripts/download-prod-abis.py | 159 ++++++++++++++++++++++ 2 files changed, 161 insertions(+), 1 deletion(-) create mode 100755 scripts/download-prod-abis.py diff --git a/derive_client/data/templates/api.py.jinja b/derive_client/data/templates/api.py.jinja index e88edad6..9757b4b3 100644 --- a/derive_client/data/templates/api.py.jinja +++ b/derive_client/data/templates/api.py.jinja @@ -3,7 +3,8 @@ import msgspec from derive_client._clients.rest{% if is_async %}.async_http{% else %}.http{% endif %}.session import {% if is_async %}AsyncHTTPSession{% else %}HTTPSession{% endif %} -from derive_client.config import EnvConfig, PUBLIC_HEADERS +from derive_client.config import PUBLIC_HEADERS +from derive_client.data_types import EnvConfig from derive_client._clients.utils import try_cast_response, AuthContext, encode_json_exclude_none from derive_client._clients.rest.endpoints import PublicEndpoints, PrivateEndpoints from derive_client.data_types.generated_models import ( diff --git a/scripts/download-prod-abis.py b/scripts/download-prod-abis.py new file mode 100755 index 00000000..eeea4f76 --- /dev/null +++ b/scripts/download-prod-abis.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python3 + +""" +Download ABIs for all Derive production contract addresses. + +This script fetches ABIs from block explorers for all contracts used in production, +including handling EIP1967 proxy contracts by detecting and downloading their +implementation ABIs as well. +""" + +import json +import sys +import time + +from web3 import Web3 + +from derive_client.config import ABI_DATA_DIR +from derive_client.data_types import ChainID, ChecksumAddress, Currency, MintableTokenData, NonMintableTokenData +from derive_client.utils.logger import get_logger +from derive_client.utils.prod_addresses import get_prod_derive_addresses +from derive_client.utils.retry import get_retry_session +from derive_client.utils.w3 import get_w3_connection + +TIMEOUT = 10 +REQUEST_DELAY = 0.5 +RATE_LIMIT_DELAY = 300 # 5 minutes +EIP1967_SLOT = (int.from_bytes(Web3.keccak(text="eip1967.proxy.implementation")[:32], "big") - 1).to_bytes(32, "big") + + +CHAIN_ID_TO_URL = { + ChainID.ETH: "https://abidata.net/{address}", + ChainID.OPTIMISM: "https://abidata.net/{address}?network=optimism", + ChainID.ARBITRUM: "https://abidata.net/{address}?network=arbitrum", + ChainID.BASE: "https://abidata.net/{address}?network=base", + ChainID.DERIVE: "https://explorer.derive.xyz/api?module=contract&action=getabi&address={address}", +} + + +def get_abi(chain_id, contract_address: str, logger): + url = CHAIN_ID_TO_URL[chain_id].format(address=contract_address) + session = get_retry_session() + + for attempt in range(2): + try: + response = session.get(url, timeout=TIMEOUT) + response.raise_for_status() + if chain_id == ChainID.DERIVE: + return json.loads(response.json()["result"]) + return response.json()["abi"] + except Exception as e: + error_str = str(e).lower() + if "rate limit" in error_str or "429" in error_str: + logger.warning( + f"Rate limit hit for {url}. ", + f"Waiting {RATE_LIMIT_DELAY}s before retry.", + ) + time.sleep(RATE_LIMIT_DELAY) + + +def collect_prod_addresses( + currencies: dict[Currency, NonMintableTokenData | MintableTokenData], +): + contract_addresses = [] + for currency, token_data in currencies.items(): + if isinstance(token_data, MintableTokenData): + contract_addresses.append(token_data.Controller) + contract_addresses.append(token_data.MintableToken) + else: # NonMintableTokenData + contract_addresses.append(token_data.Vault) + contract_addresses.append(token_data.NonMintableToken) + + if token_data.LyraTSADepositHook is not None: + contract_addresses.append(token_data.LyraTSADepositHook) + if token_data.LyraTSAShareHandlerDepositHook is not None: + contract_addresses.append(token_data.LyraTSAShareHandlerDepositHook) + + for connector_chain_id, connectors in token_data.connectors.items(): + contract_addresses.append(connectors["FAST"]) + + return contract_addresses + + +def get_impl_address(w3: Web3, address: ChecksumAddress) -> ChecksumAddress | None: + """Get EIP1967 Proxy implementation address, if any.""" + + data = w3.eth.get_storage_at(address, EIP1967_SLOT) + impl_address = Web3.to_checksum_address(data[-20:]) + + if int(impl_address, 16) == 0: + return None + + return ChecksumAddress(impl_address) + + +def main() -> int: + """Main entry point for the script.""" + logger = get_logger() + prod_addresses = get_prod_derive_addresses() + + # Collect all addresses by chain + chain_addresses: dict[ChainID, list[ChecksumAddress]] = {} + for chain_id, currencies in prod_addresses.chains.items(): + chain_addresses[chain_id] = collect_prod_addresses(currencies) + + failures: list[str] = [] + abi_path = ABI_DATA_DIR.parent / "abis" + + for chain_id, addresses in chain_addresses.items(): + if chain_id not in CHAIN_ID_TO_URL: + logger.info(f"Network not supported by abidata.net: {chain_id.name}") + continue + + proxy_mapping: dict[str, str] = {} + w3 = get_w3_connection(chain_id=chain_id) + + while addresses: + address = addresses.pop() + contract_abi_path = abi_path / chain_id.name.lower() / f"{address}.json" + + if impl_address := get_impl_address(w3=w3, address=address): + logger.info(f"EIP1967 Proxy detected: {address} -> {impl_address}") + addresses.append(impl_address) + proxy_mapping[address] = impl_address + + if not contract_abi_path.exists(): + try: + abi = get_abi(chain_id=chain_id, contract_address=address, logger=logger) + except Exception as e: + failures.append(f"{chain_id.name}: {address}: {e}") + logger.warning(f"Failed to fetch ABI for {address}: {e}") + continue + if addresses: + time.sleep(REQUEST_DELAY) + else: + logger.info(f"Already present: {contract_abi_path}") + continue + + contract_abi_path = abi_path / chain_id.name.lower() / f"{address}.json" + contract_abi_path.parent.mkdir(exist_ok=True, parents=True) + contract_abi_path.write_text(json.dumps(abi, indent=4)) + logger.info(f"Saved ABI: {contract_abi_path}") + + if proxy_mapping: + proxy_mapping_path = abi_path / chain_id.name.lower() / "proxy_mapping.json" + proxy_mapping_path.write_text(json.dumps(proxy_mapping, indent=4)) + logger.info(f"Saved proxy mapping: {proxy_mapping_path}") + + if failures: + logger.error(f"Failed to fetch {len(failures)} ABIs:") + for failure in failures: + logger.error(f" {failure}") + return 1 + + logger.info("Successfully downloaded all ABIs!") + return 0 + + +if __name__ == "__main__": + sys.exit(main())