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
48 changes: 48 additions & 0 deletions examples/create_market_order_async_client.py
Original file line number Diff line number Diff line change
@@ -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())
70 changes: 70 additions & 0 deletions examples/create_modify_cancel_order_async_nonce.py
Original file line number Diff line number Diff line change
@@ -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())
20 changes: 20 additions & 0 deletions examples/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"}))

Expand Down
74 changes: 66 additions & 8 deletions lighter/nonce_manager.py
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -18,13 +20,25 @@ 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__(
self,
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")
Expand All @@ -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)
Expand All @@ -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:
Expand All @@ -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]:
"""
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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")
24 changes: 22 additions & 2 deletions lighter/signer_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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]]:
Expand Down
Loading