Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions examples/create_market_order_async_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
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"):
# Constructing the SignerClient performs no network I/O (nonces are
# fetched lazily on first use), so it is safe inside a coroutine.
base_url, account_index, private_keys = get_api_key_config(config_file)
client = lighter.SignerClient(
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())
134 changes: 93 additions & 41 deletions lighter/nonce_manager.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import abc
import asyncio
import enum
from typing import Optional, Tuple, List
from typing import Dict, Optional, Tuple, List

import requests

from lighter.api.transaction_api import TransactionApi
from lighter.api_client import ApiClient
from lighter.errors import ValidationError


def get_nonce_from_api(client: ApiClient, account_index: int, api_key: int) -> int:
# uses request to avoid async initialization
# Blocking fallback for callers using the sync next_nonce()/refresh paths.
# The async paths never use this; they go through TransactionApi.
req = requests.get(
client.configuration.host + "/api/v1/nextNonce",
params={"account_index": account_index, "api_key_index": api_key},
Expand All @@ -20,6 +23,15 @@ def get_nonce_from_api(client: ApiClient, account_index: int, api_key: int) -> i


class NonceManager(abc.ABC):
"""
Nonces are fetched lazily on first use per api key, so constructing a
manager (and therefore a SignerClient) performs no network I/O and is safe
inside a running event loop.

next_nonce() may perform a blocking HTTP call on the first use of each api
key; inside an event loop use async_next_nonce() instead.
"""

def __init__(
self,
account_index: int,
Expand All @@ -33,10 +45,37 @@ 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: Dict[int, int] = {}
# created lazily so they bind to the running loop on older Pythons
self._locks: Dict[int, asyncio.Lock] = {}

def rotate_key(self) -> int:
self.current = (self.current + 1) % len(self.api_keys_list)
return self.api_keys_list[self.current]

def lock(self, api_key: int) -> asyncio.Lock:
lock = self._locks.get(api_key)
if lock is None:
lock = self._locks[api_key] = asyncio.Lock()
return lock

def _validate_key(self, api_key: int) -> None:
if api_key not in self.api_keys_list:
raise ValidationError(f"unknown api key index {api_key}")

async def _fetch_nonce(self, api_key: int) -> int:
resp = await TransactionApi(self.api_client).next_nonce(
account_index=self.account_index, api_key_index=api_key
)
return resp.nonce

def _ensure_nonce_sync(self, api_key: int) -> None:
if api_key not in self.nonce:
self.nonce[api_key] = get_nonce_from_api(self.api_client, self.account_index, api_key) - 1

async def _ensure_nonce(self, api_key: int) -> None:
if api_key not in self.nonce:
self.nonce[api_key] = await self._fetch_nonce(api_key) - 1

def refresh_nonce(self, api_key: int) -> int:
self.nonce[api_key] = get_nonce_from_api(self.api_client, self.account_index, api_key)
Expand All @@ -45,75 +84,81 @@ def refresh_nonce(self, api_key: int) -> int:
def hard_refresh_nonce(self, api_key: int):
self.nonce[api_key] = get_nonce_from_api(self.api_client, self.account_index, api_key) - 1

async def async_refresh_nonce(self, api_key: int) -> int:
self.nonce[api_key] = await self._fetch_nonce(api_key)
return self.nonce[api_key]

async def async_hard_refresh_nonce(self, api_key: int):
self.nonce[api_key] = await self._fetch_nonce(api_key) - 1

@abc.abstractmethod
def next_nonce(self, api_key: Optional[int] = None) -> Tuple[int, int]:
pass

@abc.abstractmethod
async def async_next_nonce(self, api_key: Optional[int] = None) -> Tuple[int, int]:
pass

def acknowledge_failure(self, api_key: int) -> None:
pass


class OptimisticNonceManager(NonceManager):
def __init__(
self,
account_index: int,
api_client: ApiClient,
api_keys_list: List[int]
) -> None:
super().__init__(account_index, api_client, api_keys_list)

def 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]
api_key = self.rotate_key()
self._validate_key(api_key)
self._ensure_nonce_sync(api_key)
self.nonce[api_key] += 1
return api_key, self.nonce[api_key]

async def async_next_nonce(self, api_key: Optional[int] = None) -> Tuple[int, int]:
if api_key is None:
api_key = self.rotate_key()
self._validate_key(api_key)
await self._ensure_nonce(api_key)
self.nonce[api_key] += 1
return api_key, self.nonce[api_key]

def acknowledge_failure(self, api_key: int) -> None:
self.nonce[api_key] -= 1
if api_key in self.nonce:
self.nonce[api_key] -= 1


class ApiNonceManager(NonceManager):
def __init__(
self,
account_index: int,
api_client: ApiClient,
api_keys_list: List[int],
) -> None:
super().__init__(account_index, api_client, api_keys_list)
"""
It is recommended to wait at least 350ms before using the same api key.
Please be mindful of your transaction frequency when using this nonce manager.
predicted_execution_time_ms from the response could give you a tighter bound.
"""

def next_nonce(self, api_key: Optional[int] = None) -> Tuple[int, int]:
"""
It is recommended to wait at least 350ms before using the same api key.
Please be mindful of your transaction frequency when using this nonce manager.
predicted_execution_time_ms from the response could give you a tighter bound.
"""
if api_key is None:
self.current = (self.current + 1) % len(self.api_keys_list)
api_key = self.api_keys_list[self.current]

api_key = self.rotate_key()
self._validate_key(api_key)
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:
api_key = self.rotate_key()
self._validate_key(api_key)
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):
# Skip super().__init__() to avoid the HTTP call
self.account_index = account_index
self.api_client = api_client
self.api_keys_list = api_keys_list
self.nonce = {}
self.current = 0

def next_nonce(self, api_key=None):
def next_nonce(self, api_key: Optional[int] = None) -> Tuple[int, int]:
raise ValidationError(
"NoOpNonceManager does not manage nonces. "
"You must provide nonce and api_key_index explicitly."
)

async def async_next_nonce(self, api_key: Optional[int] = None) -> Tuple[int, int]:
return self.next_nonce(api_key)

def acknowledge_failure(self, api_key):
pass # no-op

Expand All @@ -123,6 +168,13 @@ def refresh_nonce(self, api_key):
def hard_refresh_nonce(self, api_key):
pass # no-op

async def async_refresh_nonce(self, api_key):
pass # no-op

async def async_hard_refresh_nonce(self, api_key):
pass # no-op


class NonceManagerType(enum.Enum):
OPTIMISTIC = 1
API = 2
Expand Down
48 changes: 23 additions & 25 deletions lighter/signer_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,22 +220,31 @@ async def wrapper(self, *args, **kwargs):
# Extract api_key_index and nonce from kwargs or use defaults
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()
partial_arguments = {k: v for k, v in bound_args.arguments.items() if k not in ("self", "nonce", "api_key_index")}

# 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 is None and err) or (ret and 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)
if not (api_key_index == 255 and nonce == -1):
# The caller manages nonces explicitly; the nonce manager is not involved.
try:
return await func(self, **partial_arguments, nonce=nonce, api_key_index=api_key_index)
except lighter.exceptions.BadRequestException as e:
return None, None, trim_exc(str(e))
else:
self.nonce_manager.acknowledge_failure(api_key_index)

# Pick the key outside the lock so concurrent calls spread across keys,
# then hold the key's lock through the send so transactions on the same
# key reach the sequencer in nonce order.
api_key_index = self.nonce_manager.rotate_key()
ret: TxHash
async with self.nonce_manager.lock(api_key_index):
_, nonce = await self.nonce_manager.async_next_nonce(api_key_index)
try:
created_tx, ret, err = await func(self, **partial_arguments, nonce=nonce, api_key_index=api_key_index)
if (ret is None and err) or (ret and ret.code != CODE_OK):
self.nonce_manager.acknowledge_failure(api_key_index)
except lighter.exceptions.BadRequestException as e:
if "invalid nonce" in str(e):
await self.nonce_manager.async_hard_refresh_nonce(api_key_index)
else:
self.nonce_manager.acknowledge_failure(api_key_index)
return None, None, trim_exc(str(e))

return created_tx, ret, err
Expand Down Expand Up @@ -429,17 +438,6 @@ def check_client(self):
def create_api_key(self):
return create_api_key()

def get_api_key_nonce(self, api_key_index: int, nonce: int) -> Tuple[int, int]:
if api_key_index != self.DEFAULT_API_KEY_INDEX and nonce != self.DEFAULT_NONCE:
return api_key_index, nonce

if nonce != self.DEFAULT_NONCE:
if len(self.api_key_dict) == 1:
return self.nonce_manager.next_nonce()
else:
raise Exception("ambiguous api key")
return self.nonce_manager.next_nonce()

def create_auth_token_with_expiry(self, deadline: int = DEFAULT_10_MIN_AUTH_EXPIRY, *, timestamp: int = None, api_key_index: int = DEFAULT_API_KEY_INDEX):
if deadline == SignerClient.DEFAULT_10_MIN_AUTH_EXPIRY:
deadline = 10 * SignerClient.MINUTE
Expand Down
Loading