refactor: restructure Lighter SDK by removing deprecated Go files and enhancing Python documentation with new examples and models

This commit is contained in:
discountry
2025-12-07 19:14:56 +08:00
parent 85705dd27c
commit 5f79b134f2
310 changed files with 47112 additions and 3698 deletions
@@ -0,0 +1 @@
*.json
@@ -0,0 +1,168 @@
# Read-Only Auth Token Pre-Generation
This example demonstrates how to pre-generate authentication tokens for read-only operations on the Lighter platform. By generating tokens ahead of time, you can avoid needing access to your API private keys during runtime for read-only queries.
## Overview
Authentication tokens on Lighter have a maximum expiry of 8 hours. This example allows you to:
1. Configure a dedicated API key (index 253) for all your accounts
2. Pre-generate authentication tokens for future time periods
3. Use these tokens for read-only operations without exposing your private keys
The tokens are generated at 6-hour intervals (aligned to Unix timestamp // 6 hours), with each token valid for 8 hours. This provides an overlap period ensuring continuous coverage.
## Setup
The setup script configures API key 253 for all accounts associated with your Ethereum private key.
### Running Setup
```bash
cd examples/read-only-auth
python3 setup.py config.json
```
This will:
- Query all accounts for your L1 address
- Generate new API key pairs for each account
- Change API key 253 to use the new keys
- Output configuration in JSON format
### Configuration Variables
Edit the constants in `setup.py`:
```python
BASE_URL = "https://testnet.zklighter.elliot.ai"
ETH_PRIVATE_KEY = "your_ethereum_private_key_here"
API_KEY_INDEX = 253 # Using 253 as it's typically unused
```
### Config Format
```json
{
"BASE_URL": "https://testnet.zklighter.elliot.ai",
"ACCOUNTS": [
{
"api_key_private_key": "...",
"account_index": 0,
"api_key_index": 253
},
{
"api_key_private_key": "...",
"account_index": 1,
"api_key_index": 253
}
]
}
```
## Generating Tokens
The generation script creates authentication tokens for future time periods.
### Running Generation
```bash
NUM_DAYS=10 python3 generate.py config.json
```
If no config file is specified, it defaults to `config.json`.
### Duration Configuration
You can specify the duration in days using the `NUM_DAYS` environment variable, as in the command above.
If the value is not specified, it defaults to 28 days.
### Output Format
The script generates `auth-tokens.json`:
```json
{
"0": {
"1697184000": "auth_token_string_1",
"1697205600": "auth_token_string_2",
"1697227200": "auth_token_string_3"
},
"1": {
"1697184000": "auth_token_string_1",
"1697205600": "auth_token_string_2"
}
}
```
Where:
- First level key: account index
- Second level key: Unix timestamp (aligned to 6-hour boundaries)
- Value: authentication token
## Usage
### Looking Up Tokens
Check the `get_auth_token.py` script which prints the Auth Token that should be used **at this moment**, as this will be invalidated in at most 8 hours.
### Time Alignment
All timestamps are aligned to 6-hour boundaries:
- Timestamps are divisible by 21600 seconds (6 hours)
- Calculation: `unix_timestamp // (6 * 3600) * (6 * 3600)`
- This ensures consistent token lookup across different systems
### Token Expiry
Each token is valid for 8 hours from its timestamp:
- Token timestamp: aligned to 6-hour boundary
- Valid until: timestamp + 8 hours
- This provides 2 hours of overlap between consecutive tokens
## Security
### API Key 253
We use API key index 253 because:
- It's the last available index [0-253]
- It's not typically used by trading
- Easy to remember for this specific use case
- Easy to change and invalidate all tokens.
### Invalidating Tokens
To invalidate all existing tokens:
```bash
python3 setup.py config.json
```
Re-running the setup script generates new API keys for index 253, which invalidates all previously generated authentication tokens. This is useful if:
- You suspect your tokens have been compromised
- You want to rotate your tokens periodically
- You need to revoke access immediately
### Best Practices
1. **Store tokens securely**: The `auth-tokens.json` file contains sensitive data (read only, but still)
2. **Dedicated API key**: Use API key 253 for read-only token generation, as it can be invalidated easely.
## Troubleshooting
### "Account not found" error
Make sure your Ethereum private key corresponds to an account registered on the Lighter platform.
### "Failed to change API key" error
This could happen if:
- The API key change transaction failed
- Network connectivity issues
- The account is not active
## Additional Notes
- Tokens are specific to each account index
- Each account has its own set of time-aligned tokens
- The system uses the SignerClient's native `create_auth_token_with_expiry` method
@@ -0,0 +1,103 @@
import asyncio
import json
import logging
import os
import time
import sys
import lighter
logging.basicConfig(level=logging.INFO, force=True)
def create_auth_token_for_timestamp(signer_client, timestamp, expiry_hours):
auth_token, error = signer_client.create_auth_token_with_expiry(expiry_hours * 3600, timestamp=timestamp)
if error is not None:
raise Exception(f"Failed to create auth token: {error}")
return auth_token
async def generate_tokens_for_account(account_info, base_url, duration_days):
account_index = account_info["account_index"]
api_key_private_key = account_info["api_key_private_key"]
api_key_index = account_info["api_key_index"]
logging.info(f"Generating tokens for account {account_index}")
signer_client = lighter.SignerClient(
url=base_url,
private_key=api_key_private_key,
account_index=account_index,
api_key_index=api_key_index,
)
current_time = int(time.time())
interval_seconds = 6 * 3600
start_timestamp = (current_time // interval_seconds) * interval_seconds
num_tokens = 4 * duration_days
expiry_hours = 8
tokens = {}
for i in range(num_tokens):
timestamp = start_timestamp + (i * interval_seconds)
try:
auth_token = create_auth_token_for_timestamp(signer_client, timestamp, expiry_hours)
tokens[str(timestamp)] = auth_token
logging.debug(f"Generated token for timestamp {timestamp}")
except Exception as e:
logging.error(f"Failed to generate token for timestamp {timestamp}: {e}")
await signer_client.close()
return account_index, tokens
async def main():
config_file = "config.json"
if len(sys.argv) > 1:
config_file = sys.argv[1]
try:
with open(config_file, "r") as f:
config = json.load(f)
except FileNotFoundError:
logging.error(f"Config file '{config_file}' not found")
logging.error("Run setup.py first: python3 setup.py > config.json")
sys.exit(1)
except json.JSONDecodeError as e:
logging.error(f"Invalid JSON in config file: {e}")
sys.exit(1)
num_days = int(os.getenv("NUM_DAYS") or 28)
base_url = config.get("BASE_URL")
accounts = config.get("ACCOUNTS", [])
duration_days = config.get("DURATION_IN_DAYS", num_days)
if not base_url:
logging.error("BASE_URL not found in config")
sys.exit(1)
if not accounts:
logging.error("No accounts found in config")
sys.exit(1)
logging.info(f"Generating tokens for {len(accounts)} account(s)")
logging.info(f"Duration: {duration_days} days ({4 * duration_days} tokens per account)")
auth_tokens = {}
for account_info in accounts:
account_index, tokens = await generate_tokens_for_account(account_info, base_url, duration_days)
auth_tokens[str(account_index)] = tokens
output_file = "auth-tokens.json"
with open(output_file, "w") as f:
json.dump(auth_tokens, f, indent=2)
logging.info(f"Successfully generated tokens and saved to {output_file}")
logging.info(f"Total accounts: {len(auth_tokens)}")
for account_index, tokens in auth_tokens.items():
logging.info(f" Account {account_index}: {len(tokens)} tokens")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,30 @@
import json
import logging
import sys
import time
logging.basicConfig(level=logging.INFO, force=True)
def main():
if len(sys.argv) == 1:
logging.error("No account index specified")
return
account_index = sys.argv[1]
# Load pre-generated tokens
with open('auth-tokens.json') as f:
auth_tokens = json.load(f)
# Get current aligned timestamp (6-hour boundary)
current_timestamp = (int(time.time()) // (6 * 3600)) * (6 * 3600)
# Look up token for specific account
auth_token = auth_tokens[account_index][str(current_timestamp)]
print(f"{auth_token=}")
if __name__ == "__main__":
main()
@@ -0,0 +1,113 @@
import asyncio
import json
import logging
import sys
import time
import eth_account
import lighter
logging.basicConfig(level=logging.INFO, force=True)
# use https://mainnet.zklighter.elliot.ai for mainnet
BASE_URL = "https://testnet.zklighter.elliot.ai"
ETH_PRIVATE_KEY = "1234567812345678123456781234567812345678123456781234567812345678"
API_KEY_INDEX = 253
async def setup_account(eth_private_key, account_index, base_url, api_key_index):
private_key, public_key, err = lighter.create_api_key()
if err is not None:
return None, f"Failed to create API key for account {account_index}: {err}"
tx_client = lighter.SignerClient(
url=base_url,
private_key=private_key,
account_index=account_index,
api_key_index=api_key_index,
)
response, err = await tx_client.change_api_key(
eth_private_key=eth_private_key,
new_pubkey=public_key,
)
if err is not None:
await tx_client.close()
return None, f"Failed to change API key for account {account_index}: {err}"
time.sleep(5)
err = tx_client.check_client()
if err is not None:
await tx_client.close()
return None, f"Failed to verify API key for account {account_index}: {err}"
await tx_client.close()
return {
"api_key_private_key": private_key,
"account_index": account_index,
"api_key_index": api_key_index,
}, None
async def main():
config_file = "config.json"
if len(sys.argv) > 1:
config_file = sys.argv[1]
api_client = lighter.ApiClient(configuration=lighter.Configuration(host=BASE_URL))
eth_acc = eth_account.Account.from_key(ETH_PRIVATE_KEY)
eth_address = eth_acc.address
try:
response = await lighter.AccountApi(api_client).accounts_by_l1_address(
l1_address=eth_address
)
except lighter.ApiException as e:
if e.data.message == "account not found":
print(f"error: account not found for {eth_address}", file=__import__('sys').stderr)
await api_client.close()
return
else:
await api_client.close()
raise e
if len(response.sub_accounts) == 0:
print(f"error: no accounts found for {eth_address}", file=__import__('sys').stderr)
await api_client.close()
return
logging.info(f"Found {len(response.sub_accounts)} account(s)")
# don't do this async
accounts = []
for sub_account in response.sub_accounts:
logging.info(f"Setting up account index: {sub_account.index}")
result, err = await setup_account(
ETH_PRIVATE_KEY,
sub_account.index,
BASE_URL,
API_KEY_INDEX,
)
if err is not None:
logging.error(err)
else:
accounts.append(result)
if not accounts:
print("error: failed to setup any accounts", file=__import__('sys').stderr)
await api_client.close()
return
with open(config_file, "w", encoding="utf-8") as f:
json.dump({
"BASE_URL": BASE_URL,
"ACCOUNTS": accounts,
}, f, ensure_ascii=False, indent=2)
await api_client.close()
if __name__ == "__main__":
asyncio.run(main())