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
1 change: 1 addition & 0 deletions examples/read-only-auth/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.json
168 changes: 168 additions & 0 deletions examples/read-only-auth/README.md
Original file line number Diff line number Diff line change
@@ -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
103 changes: 103 additions & 0 deletions examples/read-only-auth/generate.py
Original file line number Diff line number Diff line change
@@ -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())
30 changes: 30 additions & 0 deletions examples/read-only-auth/get_auth_token.py
Original file line number Diff line number Diff line change
@@ -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()
Loading