Skip to content
Merged
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
3 changes: 2 additions & 1 deletion examples/create_cancel_order.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

# The API_KEY_PRIVATE_KEY provided belongs to a dummy account registered on Testnet.
# It was generated using the setup_system.py script, and servers as an example.
# Alternatively, you can go to https://app.lighter.xyz/apikeys for mainnet api keys
BASE_URL = "https://testnet.zklighter.elliot.ai"
API_KEY_PRIVATE_KEY = "0xed636277f3753b6c0275f7a28c2678a7f3a95655e09deaebec15179b50c5da7f903152e50f594f7b"
ACCOUNT_INDEX = 65
Expand Down Expand Up @@ -36,7 +37,7 @@ async def main():
market_index=0,
client_order_index=123,
base_amount=100000,
price=270000,
price=405000,
is_ask=True,
order_type=lighter.SignerClient.ORDER_TYPE_LIMIT,
time_in_force=lighter.SignerClient.ORDER_TIME_IN_FORCE_GOOD_TILL_TIME,
Expand Down
48 changes: 48 additions & 0 deletions examples/create_with_multiple_keys.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import asyncio
import lighter


BASE_URL = "https://testnet.zklighter.elliot.ai"
# use examples/system_setup.py or the apikeys page (for mainnet) to generate new api keys
KEYS = {
5: "API_PRIVATE_KEY_5",
6: "API_PRIVATE_KEY_6",
7: "API_PRIVATE_KEY_7",
}
ACCOUNT_INDEX = 100 # replace with your account_index


async def main():
client = lighter.SignerClient(
url=BASE_URL,
private_key=KEYS[5],
account_index=ACCOUNT_INDEX,
api_key_index=5,
max_api_key_index=7,
private_keys=KEYS,
)

err = client.check_client()
if err is not None:
print(f"CheckClient error: {err}")
return

for i in range(20):
res_tuple = await client.create_order(
market_index=0,
client_order_index=123 + i,
base_amount=100000 + i,
price=385000 + i,
is_ask=True,
order_type=lighter.SignerClient.ORDER_TYPE_LIMIT,
time_in_force=lighter.SignerClient.ORDER_TIME_IN_FORCE_GOOD_TILL_TIME,
reduce_only=0,
trigger_price=0,
)
print(res_tuple)

await client.cancel_all_orders(time_in_force=client.CANCEL_ALL_TIF_IMMEDIATE, time=0)


if __name__ == "__main__":
asyncio.run(main())
2 changes: 2 additions & 0 deletions lighter/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
class ValidationError(ValueError):
pass
132 changes: 132 additions & 0 deletions lighter/nonce_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import abc
import enum
from typing import Optional, Tuple

import requests

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


def get_nonce_from_api(client: api_client.ApiClient, account_index: int, api_key_index: int) -> int:
# uses request to avoid async initialization
req = requests.get(
client.configuration.host + "/api/v1/nextNonce",
params={"account_index": account_index, "api_key_index": api_key_index},
)
if req.status_code != 200:
raise Exception(f"couldn't get nonce {req.content}")
return req.json()["nonce"]


class NonceManager(abc.ABC):
def __init__(
self,
account_index: int,
api_client: api_client.ApiClient,
start_api_key: int,
end_api_key: Optional[int] = None,
):
if end_api_key is None:
end_api_key = start_api_key
if start_api_key > end_api_key or start_api_key >= 255 or end_api_key >= 255:
raise ValidationError(f"invalid range {start_api_key=} {end_api_key=}")
self.start_api_key = start_api_key
self.end_api_key = end_api_key
self.current_api_key = end_api_key # start will be used for the first tx
self.account_index = account_index
self.api_client = api_client
self.nonce = {
api_key_index: get_nonce_from_api(api_client, account_index, api_key_index) - 1
for api_key_index in range(start_api_key, end_api_key + 1)
}

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

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

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


def increment_circular(idx: int, start_idx: int, end_idx: int) -> int:
idx += 1
if idx > end_idx:
return start_idx
return idx


class OptimisticNonceManager(NonceManager):
def __init__(
self,
account_index: int,
api_client: api_client.ApiClient,
start_api_key: int,
end_api_key: Optional[int] = None,
) -> None:
super().__init__(account_index, api_client, start_api_key, end_api_key)

def next_nonce(self) -> Tuple[int, int]:
self.current_api_key = increment_circular(self.current_api_key, self.start_api_key, self.end_api_key)
self.nonce[self.current_api_key] += 1
return (self.current_api_key, self.nonce[self.current_api_key])

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


class ApiNonceManager(NonceManager):
def __init__(
self,
account_index: int,
api_client: api_client.ApiClient,
start_api_key: int,
end_api_key: Optional[int] = None,
) -> None:
super().__init__(account_index, api_client, start_api_key, end_api_key)

def next_nonce(self) -> 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.
"""
self.current_api_key = increment_circular(self.current_api_key, self.start_api_key, self.end_api_key)
self.nonce[self.current_api_key] = get_nonce_from_api(self.api_client, self.account_index, self.current_api_key)
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)


class NonceManagerType(enum.Enum):
OPTIMISTIC = 1
API = 2


def nonce_manager_factory(
nonce_manager_type: NonceManagerType,
account_index: int,
api_client: api_client.ApiClient,
start_api_key: int,
end_api_key: Optional[int] = None,
) -> NonceManager:
if nonce_manager_type == NonceManagerType.OPTIMISTIC:
return OptimisticNonceManager(
account_index=account_index,
api_client=api_client,
start_api_key=start_api_key,
end_api_key=end_api_key,
)
elif nonce_manager_type == NonceManagerType.API:
return ApiNonceManager(
account_index=account_index,
api_client=api_client,
start_api_key=start_api_key,
end_api_key=end_api_key,
)
raise ValidationError("invalid nonce manager type")
Loading