From d3888234c10e353917c935d3679c61bddd7ef361 Mon Sep 17 00:00:00 2001 From: mihaimarcu Date: Fri, 3 Jul 2026 20:21:05 +0300 Subject: [PATCH] async nonce manager --- examples/create_market_order_async_client.py | 48 ++++++++++++ .../create_modify_cancel_order_async_nonce.py | 70 ++++++++++++++++++ examples/utils.py | 20 +++++ lighter/nonce_manager.py | 74 +++++++++++++++++-- lighter/signer_client.py | 24 +++++- 5 files changed, 226 insertions(+), 10 deletions(-) create mode 100644 examples/create_market_order_async_client.py create mode 100644 examples/create_modify_cancel_order_async_nonce.py diff --git a/examples/create_market_order_async_client.py b/examples/create_market_order_async_client.py new file mode 100644 index 0000000..38234a4 --- /dev/null +++ b/examples/create_market_order_async_client.py @@ -0,0 +1,48 @@ +import asyncio +import lighter +from utils import get_api_key_config, trim_exception + + +async def create_signer_for_user(config_file="./api_key_config.json"): + base_url, account_index, private_keys = get_api_key_config(config_file) + client = await lighter.SignerClient.create_async( + url=base_url, + account_index=account_index, + api_private_keys=private_keys, + ) + + err = client.check_client() + if err is not None: + await client.close() + raise Exception(f"CheckClient error: {trim_exception(err)}") + + return client + + +async def place_market_order_for_user(config_file="./api_key_config.json"): + client = await create_signer_for_user(config_file) + + try: + # Note: change this to 2048 to trade spot ETH. Make sure you have at least 0.1 ETH to trade spot. + market_index = 0 + + tx, tx_hash, err = await client.create_market_order( + market_index=market_index, + client_order_index=0, + base_amount=100, # 0.1 ETH + avg_execution_price=4000_00, # $4000 -- worst acceptable price for the order + is_ask=False, + ) + print(f"Create Order {tx=} {tx_hash=} {err=}") + if err is not None: + raise Exception(err) + finally: + await client.close() + + +async def main(): + await place_market_order_for_user() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/create_modify_cancel_order_async_nonce.py b/examples/create_modify_cancel_order_async_nonce.py new file mode 100644 index 0000000..0816eda --- /dev/null +++ b/examples/create_modify_cancel_order_async_nonce.py @@ -0,0 +1,70 @@ +import asyncio +from utils import default_example_setup_async + + +async def main(): + setup = await default_example_setup_async() + if setup is None: + return + + client, api_client, _ = setup + + try: + # Note: change this to 2048 to trade spot ETH. Make sure you have at least 0.1 ETH to trade spot. + market_index = 0 + + # create order + api_key_index, nonce = await client.nonce_manager.async_next_nonce() + tx, tx_hash, err = await client.create_order( + market_index=market_index, + client_order_index=123, + base_amount=1000, # 0.1 ETH + price=4050_00, # $4050 + is_ask=True, + order_type=client.ORDER_TYPE_LIMIT, + time_in_force=client.ORDER_TIME_IN_FORCE_GOOD_TILL_TIME, + reduce_only=False, + trigger_price=0, + nonce=nonce, + api_key_index=api_key_index, + ) + print(f"Create Order {tx=} {tx_hash=} {err=}") + if err is not None: + raise Exception(err) + + # modify order + # use the same API key so the TX goes after the create order TX + api_key_index, nonce = await client.nonce_manager.async_next_nonce(api_key_index) + tx, tx_hash, err = await client.modify_order( + market_index=market_index, + order_index=123, + base_amount=1100, # 0.11 ETH + price=4100_00, # $4100 + trigger_price=0, + nonce=nonce, + api_key_index=api_key_index, + ) + print(f"Modify Order {tx=} {tx_hash=} {err=}") + if err is not None: + raise Exception(err) + + # cancel order + # use the same API key so the TX goes after the modify order TX + api_key_index, nonce = await client.nonce_manager.async_next_nonce(api_key_index) + tx, tx_hash, err = await client.cancel_order( + market_index=market_index, + order_index=123, + nonce=nonce, + api_key_index=api_key_index, + ) + print(f"Cancel Order {tx=} {tx_hash=} {err=}") + if err is not None: + raise Exception(err) + + finally: + await client.close() + await api_client.close() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/utils.py b/examples/utils.py index 3c63a41..ae60ff9 100644 --- a/examples/utils.py +++ b/examples/utils.py @@ -50,6 +50,26 @@ def default_example_setup(config_file="./api_key_config.json", nonce_management_ return client, api_client, websockets.connect(f"{base_url.replace('https', 'wss')}/stream") +async def default_example_setup_async(config_file="./api_key_config.json", nonce_management_type=lighter.nonce_manager.NonceManagerType.OPTIMISTIC) -> Optional[Tuple[lighter.SignerClient, lighter.ApiClient, websockets.connect]]: + logging.basicConfig(level=logging.DEBUG) + + base_url, account_index, private_keys = get_api_key_config(config_file) + api_client = lighter.ApiClient(configuration=lighter.Configuration(host=base_url)) + client = await lighter.SignerClient.create_async( + url=base_url, + account_index=account_index, + api_private_keys=private_keys, + nonce_management_type=nonce_management_type, + ) + + err = client.check_client() + if err is not None: + print(f"CheckClient error: {trim_exception(err)}") + return + + return client, api_client, websockets.connect(f"{base_url.replace('https', 'wss')}/stream") + + async def ws_ping(ws_client: websockets.ClientConnection): await ws_client.send(json.dumps({"type": "pong"})) diff --git a/lighter/nonce_manager.py b/lighter/nonce_manager.py index aedb260..a5c4cce 100644 --- a/lighter/nonce_manager.py +++ b/lighter/nonce_manager.py @@ -1,11 +1,13 @@ import abc import enum +import json from typing import Optional, Tuple, List import requests from lighter.api_client import ApiClient from lighter.errors import ValidationError +from urllib.parse import urlencode def get_nonce_from_api(client: ApiClient, account_index: int, api_key: int) -> int: @@ -18,6 +20,17 @@ def get_nonce_from_api(client: ApiClient, account_index: int, api_key: int) -> i raise Exception(f"couldn't get nonce {req.content}") return req.json()["nonce"] +async def get_nonce_from_api_async(client: ApiClient, account_index: int, api_key: int) -> int: + query = urlencode({"account_index": account_index, "api_key_index": api_key}) + resp = await client.rest_client.request( + "GET", + f"{client.configuration.host}/api/v1/nextNonce?{query}", + ) + body = await resp.read() + if resp.status != 200: + raise Exception(f"couldn't get nonce {body}") + return json.loads(body.decode("utf-8"))["nonce"] + class NonceManager(abc.ABC): def __init__( @@ -25,6 +38,7 @@ def __init__( account_index: int, api_client: ApiClient, api_keys_list: List[int], + fetch_initial_nonce: bool = True, ): if len(api_keys_list) == 0: raise ValidationError(f"No API Key provided") @@ -33,10 +47,12 @@ def __init__( self.account_index = account_index self.api_client = api_client self.api_keys_list = api_keys_list - self.nonce = { - api_keys_list[i]: get_nonce_from_api(api_client, account_index, api_keys_list[i]) - 1 - for i in range(len(api_keys_list)) - } + self.nonce = {} + if fetch_initial_nonce: + self.nonce = { + api_keys_list[i]: get_nonce_from_api(api_client, account_index, api_keys_list[i]) - 1 + for i in range(len(api_keys_list)) + } def refresh_nonce(self, api_key: int) -> int: self.nonce[api_key] = get_nonce_from_api(self.api_client, self.account_index, api_key) @@ -52,15 +68,32 @@ def next_nonce(self, api_key: Optional[int] = None) -> Tuple[int, int]: def acknowledge_failure(self, api_key: int) -> None: pass + async def async_initialize(self): + self.nonce = { + api_key: await get_nonce_from_api_async(self.api_client, self.account_index, api_key) - 1 + for api_key in self.api_keys_list + } + + async def async_refresh_nonce(self, api_key: int) -> int: + self.nonce[api_key] = await get_nonce_from_api_async(self.api_client, self.account_index, api_key) + return self.nonce[api_key] + + async def async_hard_refresh_nonce(self, api_key: int): + self.nonce[api_key] = await get_nonce_from_api_async(self.api_client, self.account_index, api_key) - 1 + + async def async_next_nonce(self, api_key: Optional[int] = None) -> Tuple[int, int]: + return self.next_nonce(api_key) + class OptimisticNonceManager(NonceManager): def __init__( self, account_index: int, api_client: ApiClient, - api_keys_list: List[int] + api_keys_list: List[int], + fetch_initial_nonce: bool = True, ) -> None: - super().__init__(account_index, api_client, api_keys_list) + super().__init__(account_index, api_client, api_keys_list, fetch_initial_nonce=fetch_initial_nonce) def next_nonce(self, api_key: Optional[int] = None) -> Tuple[int, int]: if api_key is None: @@ -80,8 +113,9 @@ def __init__( account_index: int, api_client: ApiClient, api_keys_list: List[int], + fetch_initial_nonce: bool = True, ) -> None: - super().__init__(account_index, api_client, api_keys_list) + super().__init__(account_index, api_client, api_keys_list, fetch_initial_nonce=fetch_initial_nonce) def next_nonce(self, api_key: Optional[int] = None) -> Tuple[int, int]: """ @@ -96,11 +130,19 @@ def next_nonce(self, api_key: Optional[int] = None) -> Tuple[int, int]: nonce = self.refresh_nonce(api_key) return api_key, nonce + async def async_next_nonce(self, api_key: Optional[int] = None) -> Tuple[int, int]: + if api_key is None: + self.current = (self.current + 1) % len(self.api_keys_list) + api_key = self.api_keys_list[self.current] + + nonce = await self.async_refresh_nonce(api_key) + return api_key, nonce + class NoOpNonceManager(NonceManager): """For users who provide their own nonces (skip_nonce mode).""" # noinspection PyMissingConstructor - def __init__(self, account_index, api_client, api_keys_list): + def __init__(self, account_index, api_client, api_keys_list, fetch_initial_nonce: bool = True): # Skip super().__init__() to avoid the HTTP call self.account_index = account_index self.api_client = api_client @@ -123,6 +165,18 @@ def refresh_nonce(self, api_key): def hard_refresh_nonce(self, api_key): pass # no-op + async def async_initialize(self): + pass + + async def async_refresh_nonce(self, api_key): + pass + + async def async_hard_refresh_nonce(self, api_key): + pass + + async def async_next_nonce(self, api_key=None): + return self.next_nonce(api_key) + class NonceManagerType(enum.Enum): OPTIMISTIC = 1 API = 2 @@ -134,23 +188,27 @@ def nonce_manager_factory( account_index: int, api_client: ApiClient, api_keys_list: List[int], + fetch_initial_nonce: bool = True, ) -> NonceManager: if nonce_manager_type == NonceManagerType.OPTIMISTIC: return OptimisticNonceManager( account_index=account_index, api_client=api_client, api_keys_list=api_keys_list, + fetch_initial_nonce=fetch_initial_nonce, ) elif nonce_manager_type == NonceManagerType.API: return ApiNonceManager( account_index=account_index, api_client=api_client, api_keys_list=api_keys_list, + fetch_initial_nonce=fetch_initial_nonce, ) elif nonce_manager_type == NonceManagerType.NONE: return NoOpNonceManager( account_index=account_index, api_client=api_client, api_keys_list=api_keys_list, + fetch_initial_nonce=fetch_initial_nonce, ) raise ValidationError("invalid nonce manager type") diff --git a/lighter/signer_client.py b/lighter/signer_client.py index de2e41d..17c86df 100644 --- a/lighter/signer_client.py +++ b/lighter/signer_client.py @@ -221,7 +221,7 @@ async def wrapper(self, *args, **kwargs): api_key_index = bound_args.arguments.get("api_key_index", 255) nonce = bound_args.arguments.get("nonce", -1) if api_key_index == 255 and nonce == -1: - api_key_index, nonce = self.nonce_manager.next_nonce() + api_key_index, nonce = await self.nonce_manager.async_next_nonce() # Call the original function with modified kwargs ret: TxHash @@ -232,7 +232,7 @@ async def wrapper(self, *args, **kwargs): self.nonce_manager.acknowledge_failure(api_key_index) except lighter.exceptions.BadRequestException as e: if "invalid nonce" in str(e): - self.nonce_manager.hard_refresh_nonce(api_key_index) + await self.nonce_manager.async_hard_refresh_nonce(api_key_index) return None, None, trim_exc(str(e)) else: self.nonce_manager.acknowledge_failure(api_key_index) @@ -333,6 +333,7 @@ def __init__( account_index, api_private_keys: Dict[int, str], nonce_management_type=nonce_manager.NonceManagerType.OPTIMISTIC, + fetch_initial_nonce: bool = True, ): self.url = url self.chain_id = 304 if ("mainnet" in url or "api" in url) else 300 @@ -350,10 +351,29 @@ def __init__( account_index=account_index, api_client=self.api_client, api_keys_list=list(api_private_keys.keys()), + fetch_initial_nonce=fetch_initial_nonce, ) for api_key_index in api_private_keys.keys(): self.create_client(api_key_index) + @classmethod + async def create_async( + cls, + url, + account_index, + api_private_keys: Dict[int, str], + nonce_management_type=nonce_manager.NonceManagerType.OPTIMISTIC, + ): + client = cls( + url=url, + account_index=account_index, + api_private_keys=api_private_keys, + nonce_management_type=nonce_management_type, + fetch_initial_nonce=False, + ) + await client.nonce_manager.async_initialize() + return client + # === signer helpers === @staticmethod def __decode_tx_info(result: SignedTxResponse) -> Union[Tuple[str, str, str, None], Tuple[None, None, None, str]]: