From 2deefa8fcb141640321349110b8f1fe62c3af162 Mon Sep 17 00:00:00 2001 From: Th0masi <89027829+th0masi@users.noreply.github.com> Date: Sun, 21 Sep 2025 18:01:07 +0400 Subject: [PATCH 1/5] Update signer_client.py --- lighter/signer_client.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/lighter/signer_client.py b/lighter/signer_client.py index 662ad0a..5a1af32 100644 --- a/lighter/signer_client.py +++ b/lighter/signer_client.py @@ -85,7 +85,16 @@ async def wrapper(self, *args, **kwargs): api_key_index = bound_args.arguments.get("api_key_index", -1) nonce = bound_args.arguments.get("nonce", -1) if api_key_index == -1 and nonce == -1: - api_key_index, nonce = self.nonce_manager.next_nonce() + if self.nonce_manager is None: + self.nonce_manager = nonce_manager.nonce_manager_factory( + nonce_manager_type=self._nonce_management_type, + account_index=self.account_index, + api_client=self.api_client, + start_api_key=self.api_key_index, + end_api_key=self.end_api_key_index, + ) + api_key_index, nonce = self.nonce_manager.next_nonce() + err = self.switch_api_key(api_key_index) if err != None: raise Exception(f"error switching api key: {err}") @@ -188,13 +197,9 @@ def __init__( self.api_client = lighter.ApiClient(configuration=Configuration(host=url)) self.tx_api = lighter.TransactionApi(self.api_client) self.order_api = lighter.OrderApi(self.api_client) - self.nonce_manager = nonce_manager.nonce_manager_factory( - nonce_manager_type=nonce_management_type, - account_index=account_index, - api_client=self.api_client, - start_api_key=self.api_key_index, - end_api_key=self.end_api_key_index, - ) + self._nonce_management_type = nonce_management_type + self.nonce_manager = None + for api_key in range(self.api_key_index, self.end_api_key_index + 1): self.create_client(api_key) @@ -907,6 +912,11 @@ async def send_tx(self, tx_type: StrictInt, tx_info: str) -> RespSendTx: async def close(self): await self.api_client.close() + async def __aenter__(self): + return self + async def __aexit__(self, exc_type, exc, tb): + await self.close() + @staticmethod def are_keys_equal(key1, key2) -> bool: start_index1, start_index2 = 0, 0 From c3d1a7c61aebe81179ee8de8d2b44048a3b76b95 Mon Sep 17 00:00:00 2001 From: Th0masi <89027829+th0masi@users.noreply.github.com> Date: Mon, 22 Sep 2025 14:59:31 +0400 Subject: [PATCH 2/5] Update nonce_manager.py --- lighter/nonce_manager.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lighter/nonce_manager.py b/lighter/nonce_manager.py index cf4c730..8486fd4 100644 --- a/lighter/nonce_manager.py +++ b/lighter/nonce_manager.py @@ -100,7 +100,8 @@ def next_nonce(self) -> Tuple[int, int]: return (self.current_api_key, self.nonce[self.current_api_key]) def refresh_nonce(self, api_key_index: int) -> int: - self.nonce[api_key_index] = get_nonce_from_api(self.api_client, self.start_api_key, self.end_api_key) + self.nonce[api_key_index] = get_nonce_from_api(self.api_client, self.account_index, api_key_index) + return self.nonce[api_key_index] class NonceManagerType(enum.Enum): From 314bf5aab6a6c2b7731cd1122254c20aae7418d9 Mon Sep 17 00:00:00 2001 From: Th0masi <89027829+th0masi@users.noreply.github.com> Date: Mon, 22 Sep 2025 15:01:28 +0400 Subject: [PATCH 3/5] Update signer_client.py --- lighter/signer_client.py | 50 ++++++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/lighter/signer_client.py b/lighter/signer_client.py index 5a1af32..907a1ee 100644 --- a/lighter/signer_client.py +++ b/lighter/signer_client.py @@ -1,4 +1,5 @@ import ctypes +import asyncio from functools import wraps import inspect import json @@ -93,26 +94,28 @@ async def wrapper(self, *args, **kwargs): start_api_key=self.api_key_index, end_api_key=self.end_api_key_index, ) - api_key_index, nonce = self.nonce_manager.next_nonce() - - err = self.switch_api_key(api_key_index) - if err != None: - raise Exception(f"error switching api key: {err}") - - # Call the original function with modified kwargs - ret: TxHash - try: - partial_arguments = {k: v for k, v in bound_args.arguments.items() if k not in ("self", "nonce", "api_key_index")} - created_tx, ret, err = await func(self, **partial_arguments, nonce=nonce, api_key_index=api_key_index) - if ret.code != CODE_OK: - 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) - return None, None, trim_exc(str(e)) - else: - self.nonce_manager.acknowledge_failure(api_key_index) - return None, None, trim_exc(str(e)) + api_key_index, nonce = self.nonce_manager.next_nonce() + + # Сериализуем переключение ключа и подпись одной операции + async with self._sign_lock: + err = self.switch_api_key(api_key_index) + if err is not None: + raise Exception(f"error switching api key: {err}") + + # Call the original function with modified kwargs + ret: TxHash + try: + partial_arguments = {k: v for k, v in bound_args.arguments.items() if k not in ("self", "nonce", "api_key_index")} + created_tx, ret, err = await func(self, **partial_arguments, nonce=nonce, api_key_index=api_key_index) + if ret.code != CODE_OK: + 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) + return None, None, trim_exc(str(e)) + else: + self.nonce_manager.acknowledge_failure(api_key_index) + return None, None, trim_exc(str(e)) return created_tx, ret, err @@ -199,6 +202,7 @@ def __init__( self.order_api = lighter.OrderApi(self.api_client) self._nonce_management_type = nonce_management_type self.nonce_manager = None + self._sign_lock = asyncio.Lock() for api_key in range(self.api_key_index, self.end_api_key_index + 1): self.create_client(api_key) @@ -259,7 +263,7 @@ def check_client(self): def switch_api_key(self, api_key: int): self.signer.SwitchAPIKey.argtypes = [ctypes.c_int] - self.signer.CheckClient.restype = ctypes.c_char_p + self.signer.SwitchAPIKey.restype = ctypes.c_char_p result = self.signer.SwitchAPIKey(api_key) return result.decode("utf-8") if result else None @@ -270,8 +274,8 @@ def create_api_key(self, seed=""): self.signer.GenerateAPIKey.restype = ApiKeyResponse result = self.signer.GenerateAPIKey(ctypes.c_char_p(seed.encode("utf-8"))) - private_key_str = result.str.decode("utf-8") if result.privateKey else None - public_key_str = result.str.decode("utf-8") if result.publicKey else None + private_key_str = result.privateKey.decode("utf-8") if result.privateKey else None + public_key_str = result.publicKey.decode("utf-8") if result.publicKey else None error = result.err.decode("utf-8") if result.err else None return private_key_str, public_key_str, error From 1daa0ec5ad150f65390de3f5e441c012b07876d7 Mon Sep 17 00:00:00 2001 From: Th0masi <89027829+th0masi@users.noreply.github.com> Date: Mon, 22 Sep 2025 15:41:22 +0400 Subject: [PATCH 4/5] Update signer_client.py --- lighter/signer_client.py | 95 ++++++++++++++++++++++++++-------------- 1 file changed, 61 insertions(+), 34 deletions(-) diff --git a/lighter/signer_client.py b/lighter/signer_client.py index 907a1ee..aad1353 100644 --- a/lighter/signer_client.py +++ b/lighter/signer_client.py @@ -1,5 +1,6 @@ import ctypes import asyncio +import threading from functools import wraps import inspect import json @@ -24,6 +25,8 @@ CODE_OK = 200 +_GLOBAL_SIGN_LOCK = threading.RLock() + class ApiKeyResponse(ctypes.Structure): _fields_ = [("privateKey", ctypes.c_char_p), ("publicKey", ctypes.c_char_p), ("err", ctypes.c_char_p)] @@ -76,13 +79,10 @@ def trim_exc(exception_body: str): def process_api_key_and_nonce(func): @wraps(func) async def wrapper(self, *args, **kwargs): - # Get the signature sig = inspect.signature(func) - # Bind args and kwargs to the function's signature bound_args = sig.bind(self, *args, **kwargs) bound_args.apply_defaults() - # Extract api_key_index and nonce from kwargs or use defaults api_key_index = bound_args.arguments.get("api_key_index", -1) nonce = bound_args.arguments.get("nonce", -1) if api_key_index == -1 and nonce == -1: @@ -96,13 +96,11 @@ async def wrapper(self, *args, **kwargs): ) api_key_index, nonce = self.nonce_manager.next_nonce() - # Сериализуем переключение ключа и подпись одной операции async with self._sign_lock: err = self.switch_api_key(api_key_index) if err is not None: raise Exception(f"error switching api key: {err}") - # Call the original function with modified kwargs ret: TxHash try: partial_arguments = {k: v for k, v in bound_args.arguments.items() if k not in ("self", "nonce", "api_key_index")} @@ -180,7 +178,6 @@ def __init__( """ chain_id = 304 if "mainnet" in url else 300 - # api_key_index=0 is generally used by frontend if private_key.startswith("0x"): private_key = private_key[2:] self.url = url @@ -286,7 +283,10 @@ def sign_change_api_key(self, eth_private_key, new_pubkey: str, nonce: int): ctypes.c_longlong, ] self.signer.SignChangePubKey.restype = StrOrErr - result = self.signer.SignChangePubKey(ctypes.c_char_p(new_pubkey.encode("utf-8")), nonce) + with _GLOBAL_SIGN_LOCK: + # Обновляем контекст на (api_key_index, account_index) + self.create_client(self.api_key_index) + result = self.signer.SignChangePubKey(ctypes.c_char_p(new_pubkey.encode("utf-8")), nonce) tx_info_str = result.str.decode("utf-8") if result.str else None error = result.err.decode("utf-8") if result.err else None @@ -344,19 +344,21 @@ def sign_create_order( ] self.signer.SignCreateOrder.restype = StrOrErr - result = self.signer.SignCreateOrder( - market_index, - client_order_index, - base_amount, - price, - int(is_ask), - order_type, - time_in_force, - reduce_only, - trigger_price, - order_expiry, - nonce, - ) + with _GLOBAL_SIGN_LOCK: + self.create_client(self.api_key_index) + result = self.signer.SignCreateOrder( + market_index, + client_order_index, + base_amount, + price, + int(is_ask), + order_type, + time_in_force, + reduce_only, + trigger_price, + order_expiry, + nonce, + ) tx_info = result.str.decode("utf-8") if result.str else None error = result.err.decode("utf-8") if result.err else None @@ -371,7 +373,9 @@ def sign_cancel_order(self, market_index, order_index, nonce=-1): ] self.signer.SignCancelOrder.restype = StrOrErr - result = self.signer.SignCancelOrder(market_index, order_index, nonce) + with _GLOBAL_SIGN_LOCK: + self.create_client(self.api_key_index) + result = self.signer.SignCancelOrder(market_index, order_index, nonce) tx_info = result.str.decode("utf-8") if result.str else None error = result.err.decode("utf-8") if result.err else None @@ -382,7 +386,9 @@ def sign_withdraw(self, usdc_amount, nonce=-1): self.signer.SignWithdraw.argtypes = [ctypes.c_longlong, ctypes.c_longlong] self.signer.SignWithdraw.restype = StrOrErr - result = self.signer.SignWithdraw(usdc_amount, nonce) + with _GLOBAL_SIGN_LOCK: + self.create_client(self.api_key_index) + result = self.signer.SignWithdraw(usdc_amount, nonce) tx_info = result.str.decode("utf-8") if result.str else None error = result.err.decode("utf-8") if result.err else None @@ -393,7 +399,9 @@ def sign_create_sub_account(self, nonce=-1): self.signer.SignCreateSubAccount.argtypes = [ctypes.c_longlong] self.signer.SignCreateSubAccount.restype = StrOrErr - result = self.signer.SignCreateSubAccount(nonce) + with _GLOBAL_SIGN_LOCK: + self.create_client(self.api_key_index) + result = self.signer.SignCreateSubAccount(nonce) tx_info = result.str.decode("utf-8") if result.str else None error = result.err.decode("utf-8") if result.err else None @@ -408,7 +416,9 @@ def sign_cancel_all_orders(self, time_in_force, time, nonce=-1): ] self.signer.SignCancelAllOrders.restype = StrOrErr - result = self.signer.SignCancelAllOrders(time_in_force, time, nonce) + with _GLOBAL_SIGN_LOCK: + self.create_client(self.api_key_index) + result = self.signer.SignCancelAllOrders(time_in_force, time, nonce) tx_info = result.str.decode("utf-8") if result.str else None error = result.err.decode("utf-8") if result.err else None @@ -426,7 +436,9 @@ def sign_modify_order(self, market_index, order_index, base_amount, price, trigg ] self.signer.SignModifyOrder.restype = StrOrErr - result = self.signer.SignModifyOrder(market_index, order_index, base_amount, price, trigger_price, nonce) + with _GLOBAL_SIGN_LOCK: + self.create_client(self.api_key_index) + result = self.signer.SignModifyOrder(market_index, order_index, base_amount, price, trigger_price, nonce) tx_info = result.str.decode("utf-8") if result.str else None error = result.err.decode("utf-8") if result.err else None @@ -442,7 +454,9 @@ def sign_transfer(self, eth_private_key, to_account_index, usdc_amount, fee, mem ctypes.c_longlong, ] self.signer.SignTransfer.restype = StrOrErr - result = self.signer.SignTransfer(to_account_index, usdc_amount, fee, ctypes.c_char_p(memo.encode("utf-8")), nonce) + with _GLOBAL_SIGN_LOCK: + self.create_client(self.api_key_index) + result = self.signer.SignTransfer(to_account_index, usdc_amount, fee, ctypes.c_char_p(memo.encode("utf-8")), nonce) tx_info_str = result.str.decode("utf-8") if result.str else None error = result.err.decode("utf-8") if result.err else None @@ -471,7 +485,9 @@ def sign_create_public_pool(self, operator_fee, initial_total_shares, min_operat ] self.signer.SignCreatePublicPool.restype = StrOrErr - result = self.signer.SignCreatePublicPool(operator_fee, initial_total_shares, min_operator_share_rate, nonce) + with _GLOBAL_SIGN_LOCK: + self.create_client(self.api_key_index) + result = self.signer.SignCreatePublicPool(operator_fee, initial_total_shares, min_operator_share_rate, nonce) tx_info = result.str.decode("utf-8") if result.str else None error = result.err.decode("utf-8") if result.err else None @@ -488,9 +504,11 @@ def sign_update_public_pool(self, public_pool_index, status, operator_fee, min_o ] self.signer.SignUpdatePublicPool.restype = StrOrErr - result = self.signer.SignUpdatePublicPool( - public_pool_index, status, operator_fee, min_operator_share_rate, nonce - ) + with _GLOBAL_SIGN_LOCK: + self.create_client(self.api_key_index) + result = self.signer.SignUpdatePublicPool( + public_pool_index, status, operator_fee, min_operator_share_rate, nonce + ) tx_info = result.str.decode("utf-8") if result.str else None error = result.err.decode("utf-8") if result.err else None @@ -505,7 +523,9 @@ def sign_mint_shares(self, public_pool_index, share_amount, nonce=-1): ] self.signer.SignMintShares.restype = StrOrErr - result = self.signer.SignMintShares(public_pool_index, share_amount, nonce) + with _GLOBAL_SIGN_LOCK: + self.create_client(self.api_key_index) + result = self.signer.SignMintShares(public_pool_index, share_amount, nonce) tx_info = result.str.decode("utf-8") if result.str else None error = result.err.decode("utf-8") if result.err else None @@ -520,7 +540,9 @@ def sign_burn_shares(self, public_pool_index, share_amount, nonce=-1): ] self.signer.SignBurnShares.restype = StrOrErr - result = self.signer.SignBurnShares(public_pool_index, share_amount, nonce) + with _GLOBAL_SIGN_LOCK: + self.create_client(self.api_key_index) + result = self.signer.SignBurnShares(public_pool_index, share_amount, nonce) tx_info = result.str.decode("utf-8") if result.str else None error = result.err.decode("utf-8") if result.err else None @@ -535,7 +557,9 @@ def sign_update_leverage(self, market_index, fraction, margin_mode, nonce=-1): ctypes.c_longlong, ] self.signer.SignUpdateLeverage.restype = StrOrErr - result = self.signer.SignUpdateLeverage(market_index, fraction, margin_mode, nonce) + with _GLOBAL_SIGN_LOCK: + self.create_client(self.api_key_index) + result = self.signer.SignUpdateLeverage(market_index, fraction, margin_mode, nonce) tx_info = result.str.decode("utf-8") if result.str else None error = result.err.decode("utf-8") if result.err else None @@ -546,7 +570,9 @@ def create_auth_token_with_expiry(self, deadline: int = DEFAULT_10_MIN_AUTH_EXPI deadline = int(time.time() + 10 * SignerClient.MINUTE) self.signer.CreateAuthToken.argtypes = [ctypes.c_longlong] self.signer.CreateAuthToken.restype = StrOrErr - result = self.signer.CreateAuthToken(deadline) + with _GLOBAL_SIGN_LOCK: + self.create_client(self.api_key_index) + result = self.signer.CreateAuthToken(deadline) auth = result.str.decode("utf-8") if result.str else None error = result.err.decode("utf-8") if result.err else None @@ -918,6 +944,7 @@ async def close(self): async def __aenter__(self): return self + async def __aexit__(self, exc_type, exc, tb): await self.close() From 5c2bcdd4e7a8eb4cb54a648374ed372952822746 Mon Sep 17 00:00:00 2001 From: Th0masi <89027829+th0masi@users.noreply.github.com> Date: Mon, 22 Sep 2025 15:41:40 +0400 Subject: [PATCH 5/5] Update signer_client.py --- lighter/signer_client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/lighter/signer_client.py b/lighter/signer_client.py index aad1353..dbf2958 100644 --- a/lighter/signer_client.py +++ b/lighter/signer_client.py @@ -284,7 +284,6 @@ def sign_change_api_key(self, eth_private_key, new_pubkey: str, nonce: int): ] self.signer.SignChangePubKey.restype = StrOrErr with _GLOBAL_SIGN_LOCK: - # Обновляем контекст на (api_key_index, account_index) self.create_client(self.api_key_index) result = self.signer.SignChangePubKey(ctypes.c_char_p(new_pubkey.encode("utf-8")), nonce)