diff --git a/examples/system_setup.py b/examples/system_setup.py index 859274b..294c41d 100644 --- a/examples/system_setup.py +++ b/examples/system_setup.py @@ -4,11 +4,13 @@ import eth_account import lighter from utils import save_api_key_config +from lighter import get_endpoint_profile, MAINNET, ROBINHOOD, TESTNET logging.basicConfig(level=logging.DEBUG) # this is a dummy private key registered on Testnet. # It serves as a good example +ENDPOINT_PROFILE = TESTNET BASE_URL = "https://testnet.zklighter.elliot.ai" ETH_PRIVATE_KEY = "1234567812345678123456781234567812345678123456781234567812345678" API_KEY_INDEX = 3 @@ -21,7 +23,9 @@ async def main(): # verify that the account exists & fetch account index - api_client = lighter.ApiClient(configuration=lighter.Configuration(host=BASE_URL)) + api_client = lighter.ApiClient( + configuration=lighter.Configuration(host=ENDPOINT_PROFILE.api_url) + ) eth_acc = eth_account.Account.from_key(ETH_PRIVATE_KEY) eth_address = eth_acc.address @@ -63,9 +67,10 @@ async def main(): private_keys[API_KEY_INDEX + i] = private_key tx_client = lighter.SignerClient( - url=BASE_URL, + url=ENDPOINT_PROFILE.api_url, account_index=account_index, api_private_keys=private_keys, + chain_id=ENDPOINT_PROFILE.chain_id, ) # change all API keys @@ -73,7 +78,7 @@ async def main(): response, err = await tx_client.change_api_key( eth_private_key=ETH_PRIVATE_KEY, new_pubkey=public_keys[i], - api_key_index=API_KEY_INDEX + i + api_key_index=API_KEY_INDEX + i, ) if err is not None: raise Exception(err) @@ -86,7 +91,7 @@ async def main(): if err is not None: raise Exception(err) - save_api_key_config(BASE_URL, account_index, private_keys) + save_api_key_config(ENDPOINT_PROFILE, account_index, private_keys) await tx_client.close() await api_client.close() diff --git a/examples/utils.py b/examples/utils.py index 3c63a41..168a2e4 100644 --- a/examples/utils.py +++ b/examples/utils.py @@ -9,10 +9,10 @@ def trim_exception(e: Exception) -> str: return str(e).strip().split("\n")[-1] -def save_api_key_config(base_url, account_index, private_keys, config_file="./api_key_config.json"): +def save_api_key_config(endpoint_profile, account_index, private_keys, config_file="./api_key_config.json"): with open(config_file, "w", encoding="utf-8") as f: json.dump({ - "baseUrl": base_url, + "endpointProfile": endpoint_profile.name, "accountIndex": account_index, "privateKeys": private_keys, }, f, ensure_ascii=False, indent=2) @@ -27,28 +27,51 @@ def get_api_key_config(config_file="./api_key_config.json"): for key in private_keys_original.keys(): private_key[int(key)] = private_keys_original[key] - return cfg["baseUrl"], cfg["accountIndex"], private_key + profile_name = cfg.get("endpointProfile") + if profile_name: + profile = lighter.get_endpoint_profile(profile_name) + else: + # Backward compat: build a profile from the raw baseUrl + base_url = cfg["baseUrl"] + profile = lighter.EndpointProfile( + name="custom", + api_url=base_url, + ws_url=base_url.replace("https", "wss") + "/stream", + chain_id=304 if ("mainnet" in base_url) else 466324 if ("api.rh.lighter" in base_url) else 300, + ) + + return profile, cfg["accountIndex"], private_key -def default_example_setup(config_file="./api_key_config.json", nonce_management_type=lighter.nonce_manager.NonceManagerType.OPTIMISTIC) -> Optional[Tuple[lighter.SignerClient, lighter.ApiClient, websockets.connect]]: +def default_example_setup(config_file="./api_key_config.json",nonce_management_type=lighter.nonce_manager.NonceManagerType.OPTIMISTIC,endpoint_profile=None,) -> 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)) + config_endpoint, account_index, private_keys = get_api_key_config(config_file) + + profile = endpoint_profile or config_endpoint + + api_url = profile.api_url + chain_id = profile.chain_id + ws_url = profile.ws_url + + api_client = lighter.ApiClient( + configuration=lighter.Configuration(host=api_url) + ) + client = lighter.SignerClient( - url=base_url, + url=api_url, account_index=account_index, api_private_keys=private_keys, nonce_management_type=nonce_management_type, + chain_id=chain_id, ) 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") + return None + return client, api_client, websockets.connect(ws_url) async def ws_ping(ws_client: websockets.ClientConnection): await ws_client.send(json.dumps({"type": "pong"})) diff --git a/lighter/__init__.py b/lighter/__init__.py index 3f84d93..d7550a2 100644 --- a/lighter/__init__.py +++ b/lighter/__init__.py @@ -170,6 +170,7 @@ from lighter.models.withdraw_history import WithdrawHistory from lighter.models.withdraw_history_item import WithdrawHistoryItem from lighter.models.zk_lighter_info import ZkLighterInfo +from lighter.endpoint_profiles import (EndpointProfile, MAINNET, ROBINHOOD, TESTNET, DEFAULT_ENDPOINT_PROFILE, ENDPOINT_PROFILES, get_endpoint_profile) # manual additions from lighter.ws_client import WsClient diff --git a/lighter/endpoint_profiles.py b/lighter/endpoint_profiles.py new file mode 100644 index 0000000..1024c52 --- /dev/null +++ b/lighter/endpoint_profiles.py @@ -0,0 +1,61 @@ +# lighter/endpoint_profiles.py + +from dataclasses import dataclass + + +def normalize_base_url(url: str) -> str: + return url.rstrip("/") + + +def join_url(base_url: str, path: str) -> str: + return f"{base_url.rstrip('/')}/{path.lstrip('/')}" + + +@dataclass(frozen=True) +class EndpointProfile: + name: str + api_url: str + ws_url: str + chain_id: int + + def __post_init__(self): + object.__setattr__(self, "api_url", normalize_base_url(self.api_url)) + object.__setattr__(self, "ws_url", normalize_base_url(self.ws_url)) + + +MAINNET = EndpointProfile( + name="mainnet", + api_url="https://mainnet.zklighter.elliot.ai", + ws_url="wss://mainnet.zklighter.elliot.ai/stream", + chain_id=304, +) + +ROBINHOOD = EndpointProfile( + name="robinhood", + api_url="https://api.rh.lighter.xyz", + ws_url="wss://api.rh.lighter.xyz/stream", + chain_id=466324, +) + + +TESTNET = EndpointProfile( + name="testnet", + api_url="https://testnet.zklighter.elliot.ai", + ws_url="wss://testnet.zklighter.elliot.ai/stream", + chain_id=300, +) + +DEFAULT_ENDPOINT_PROFILE = MAINNET + +ENDPOINT_PROFILES = { + MAINNET.name: MAINNET, + ROBINHOOD.name: ROBINHOOD, + TESTNET.name: TESTNET, +} + + +def get_endpoint_profile(name: str) -> EndpointProfile: + try: + return ENDPOINT_PROFILES[name] + except KeyError as exc: + raise ValueError(f"Unknown endpoint profile: {name}") from exc \ No newline at end of file diff --git a/lighter/signer_client.py b/lighter/signer_client.py index de2e41d..f4ee022 100644 --- a/lighter/signer_client.py +++ b/lighter/signer_client.py @@ -333,10 +333,15 @@ def __init__( account_index, api_private_keys: Dict[int, str], nonce_management_type=nonce_manager.NonceManagerType.OPTIMISTIC, + chain_id: Optional[int] = None, ): self.url = url - self.chain_id = 304 if ("mainnet" in url or "api" in url) else 300 - + self.chain_id = chain_id if chain_id is not None else ( + 304 if ("mainnet.zklighter" in self.url) else + 300 if ("testnet.zklighter" in self.url) else + 466324 if ("api.rh.lighter" in self.url) else + 304 + ) self.validate_api_private_keys(api_private_keys) self.api_key_dict = api_private_keys self.account_index = account_index diff --git a/lighter/ws_client.py b/lighter/ws_client.py index 1369417..14abdf5 100644 --- a/lighter/ws_client.py +++ b/lighter/ws_client.py @@ -2,6 +2,7 @@ from websockets.sync.client import connect from websockets.client import connect as connect_async from lighter.configuration import Configuration +from lighter.endpoint_profiles import join_url class WsClient: def __init__( @@ -12,11 +13,14 @@ def __init__( account_ids=[], on_order_book_update=print, on_account_update=print, + ws_url=None, ): - if host is None: - host = Configuration.get_default().host.replace("https://", "") - - self.base_url = f"wss://{host}{path}" + if ws_url is not None: + self.base_url = ws_url.rstrip("/") + else: + if host is None: + host = Configuration.get_default().host.replace("https://", "") + self.base_url = join_url(f"wss://{host}", path) self.subscriptions = { "order_books": order_book_ids,