Skip to content

Commit f9cf85d

Browse files
authored
Nonce management (#18)
* nonce management * update dependencies
1 parent 7bfe3f1 commit f9cf85d

11 files changed

Lines changed: 351 additions & 21 deletions

examples/create_cancel_order.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
# The API_KEY_PRIVATE_KEY provided belongs to a dummy account registered on Testnet.
88
# It was generated using the setup_system.py script, and servers as an example.
9+
# Alternatively, you can go to https://app.lighter.xyz/apikeys for mainnet api keys
910
BASE_URL = "https://testnet.zklighter.elliot.ai"
1011
API_KEY_PRIVATE_KEY = "0xed636277f3753b6c0275f7a28c2678a7f3a95655e09deaebec15179b50c5da7f903152e50f594f7b"
1112
ACCOUNT_INDEX = 65
@@ -36,7 +37,7 @@ async def main():
3637
market_index=0,
3738
client_order_index=123,
3839
base_amount=100000,
39-
price=270000,
40+
price=405000,
4041
is_ask=True,
4142
order_type=lighter.SignerClient.ORDER_TYPE_LIMIT,
4243
time_in_force=lighter.SignerClient.ORDER_TIME_IN_FORCE_GOOD_TILL_TIME,
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import asyncio
2+
import lighter
3+
4+
5+
BASE_URL = "https://testnet.zklighter.elliot.ai"
6+
# use examples/system_setup.py or the apikeys page (for mainnet) to generate new api keys
7+
KEYS = {
8+
5: "API_PRIVATE_KEY_5",
9+
6: "API_PRIVATE_KEY_6",
10+
7: "API_PRIVATE_KEY_7",
11+
}
12+
ACCOUNT_INDEX = 100 # replace with your account_index
13+
14+
15+
async def main():
16+
client = lighter.SignerClient(
17+
url=BASE_URL,
18+
private_key=KEYS[5],
19+
account_index=ACCOUNT_INDEX,
20+
api_key_index=5,
21+
max_api_key_index=7,
22+
private_keys=KEYS,
23+
)
24+
25+
err = client.check_client()
26+
if err is not None:
27+
print(f"CheckClient error: {err}")
28+
return
29+
30+
for i in range(20):
31+
res_tuple = await client.create_order(
32+
market_index=0,
33+
client_order_index=123 + i,
34+
base_amount=100000 + i,
35+
price=385000 + i,
36+
is_ask=True,
37+
order_type=lighter.SignerClient.ORDER_TYPE_LIMIT,
38+
time_in_force=lighter.SignerClient.ORDER_TIME_IN_FORCE_GOOD_TILL_TIME,
39+
reduce_only=0,
40+
trigger_price=0,
41+
)
42+
print(res_tuple)
43+
44+
await client.cancel_all_orders(time_in_force=client.CANCEL_ALL_TIF_IMMEDIATE, time=0)
45+
46+
47+
if __name__ == "__main__":
48+
asyncio.run(main())

lighter/errors.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
class ValidationError(ValueError):
2+
pass

lighter/nonce_manager.py

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import abc
2+
import enum
3+
from typing import Optional, Tuple
4+
5+
import requests
6+
7+
from lighter import api_client
8+
from lighter.api import transaction_api
9+
from lighter.errors import ValidationError
10+
11+
12+
def get_nonce_from_api(client: api_client.ApiClient, account_index: int, api_key_index: int) -> int:
13+
# uses request to avoid async initialization
14+
req = requests.get(
15+
client.configuration.host + "/api/v1/nextNonce",
16+
params={"account_index": account_index, "api_key_index": api_key_index},
17+
)
18+
if req.status_code != 200:
19+
raise Exception(f"couldn't get nonce {req.content}")
20+
return req.json()["nonce"]
21+
22+
23+
class NonceManager(abc.ABC):
24+
def __init__(
25+
self,
26+
account_index: int,
27+
api_client: api_client.ApiClient,
28+
start_api_key: int,
29+
end_api_key: Optional[int] = None,
30+
):
31+
if end_api_key is None:
32+
end_api_key = start_api_key
33+
if start_api_key > end_api_key or start_api_key >= 255 or end_api_key >= 255:
34+
raise ValidationError(f"invalid range {start_api_key=} {end_api_key=}")
35+
self.start_api_key = start_api_key
36+
self.end_api_key = end_api_key
37+
self.current_api_key = end_api_key # start will be used for the first tx
38+
self.account_index = account_index
39+
self.api_client = api_client
40+
self.nonce = {
41+
api_key_index: get_nonce_from_api(api_client, account_index, api_key_index) - 1
42+
for api_key_index in range(start_api_key, end_api_key + 1)
43+
}
44+
45+
def hard_refresh_nonce(self, api_key: int):
46+
self.nonce[api_key] = get_nonce_from_api(self.api_client, self.account_index, api_key) - 1
47+
48+
@abc.abstractmethod
49+
def next_nonce(self) -> Tuple[int, int]:
50+
pass
51+
52+
def acknowledge_failure(self, api_key_index: int) -> None:
53+
pass
54+
55+
56+
def increment_circular(idx: int, start_idx: int, end_idx: int) -> int:
57+
idx += 1
58+
if idx > end_idx:
59+
return start_idx
60+
return idx
61+
62+
63+
class OptimisticNonceManager(NonceManager):
64+
def __init__(
65+
self,
66+
account_index: int,
67+
api_client: api_client.ApiClient,
68+
start_api_key: int,
69+
end_api_key: Optional[int] = None,
70+
) -> None:
71+
super().__init__(account_index, api_client, start_api_key, end_api_key)
72+
73+
def next_nonce(self) -> Tuple[int, int]:
74+
self.current_api_key = increment_circular(self.current_api_key, self.start_api_key, self.end_api_key)
75+
self.nonce[self.current_api_key] += 1
76+
return (self.current_api_key, self.nonce[self.current_api_key])
77+
78+
def acknowledge_failure(self, api_key_index: int) -> None:
79+
self.nonce[api_key_index] -= 1
80+
81+
82+
class ApiNonceManager(NonceManager):
83+
def __init__(
84+
self,
85+
account_index: int,
86+
api_client: api_client.ApiClient,
87+
start_api_key: int,
88+
end_api_key: Optional[int] = None,
89+
) -> None:
90+
super().__init__(account_index, api_client, start_api_key, end_api_key)
91+
92+
def next_nonce(self) -> Tuple[int, int]:
93+
"""
94+
It is recommended to wait at least 350ms before using the same api key.
95+
Please be mindful of your transaction frequency when using this nonce manager.
96+
predicted_execution_time_ms from the response could give you a tighter bound.
97+
"""
98+
self.current_api_key = increment_circular(self.current_api_key, self.start_api_key, self.end_api_key)
99+
self.nonce[self.current_api_key] = get_nonce_from_api(self.api_client, self.account_index, self.current_api_key)
100+
return (self.current_api_key, self.nonce[self.current_api_key])
101+
102+
def refresh_nonce(self, api_key_index: int) -> int:
103+
self.nonce[api_key_index] = get_nonce_from_api(self.api_client, self.start_api_key, self.end_api_key)
104+
105+
106+
class NonceManagerType(enum.Enum):
107+
OPTIMISTIC = 1
108+
API = 2
109+
110+
111+
def nonce_manager_factory(
112+
nonce_manager_type: NonceManagerType,
113+
account_index: int,
114+
api_client: api_client.ApiClient,
115+
start_api_key: int,
116+
end_api_key: Optional[int] = None,
117+
) -> NonceManager:
118+
if nonce_manager_type == NonceManagerType.OPTIMISTIC:
119+
return OptimisticNonceManager(
120+
account_index=account_index,
121+
api_client=api_client,
122+
start_api_key=start_api_key,
123+
end_api_key=end_api_key,
124+
)
125+
elif nonce_manager_type == NonceManagerType.API:
126+
return ApiNonceManager(
127+
account_index=account_index,
128+
api_client=api_client,
129+
start_api_key=start_api_key,
130+
end_api_key=end_api_key,
131+
)
132+
raise ValidationError("invalid nonce manager type")

0 commit comments

Comments
 (0)