diff --git a/examples/onboard_direct.py b/examples/onboard_direct.py new file mode 100644 index 0000000..4674e0f --- /dev/null +++ b/examples/onboard_direct.py @@ -0,0 +1,306 @@ +import asyncio +import os +import json +import time +import lighter +import requests +from typing import Optional, Dict, Tuple +from eth_account import Account +from eth_utils import to_checksum_address +from eth_abi import encode +from utils import save_api_key_config + +# Constants +ASSET_INDEX_USDC = 3 +ROUTE_TYPE_PERP = 0 +USDC_CONTRACT = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" +ZKLIGHTER_CONTRACT = "0x3B4D794a66304F130a4Db8F2551B0070dfCf5ca7" + +# Environment variables +BASE_URL = os.getenv("BASE_URL", "https://testnet.zklighter.elliot.ai") +ETH_PRIVATE_KEY = os.getenv("ETH_PRIVATE_KEY", "") +L1_RPC_URL = os.getenv("L1_RPC_URL", "") +DEPOSIT_AMOUNT = float(os.getenv("DEPOSIT_AMOUNT", "6")) +OUTPUT_FILE = "api_key_config.json" +API_KEY_INDEX = int(os.getenv("API_KEY_INDEX", "4")) +NUM_API_KEYS = int(os.getenv("NUM_API_KEYS", "1")) + +async def get_account(l1_address: str) -> Optional[Dict]: + """Get account info including balance.""" + try: + l1_address = to_checksum_address(l1_address) + api_client = lighter.ApiClient(configuration=lighter.Configuration(host=BASE_URL)) + account_api = lighter.AccountApi(api_client) + response = await account_api.accounts_by_l1_address(l1_address=l1_address) + if response.sub_accounts and len(response.sub_accounts) > 0: + accounts = response.sub_accounts + master_account = min(accounts, key=lambda x: int(x.index)) + idx = int(master_account.index) + if idx == 0: + await api_client.close() + return None + account_details = await account_api.account(by="index", value=str(idx)) + await api_client.close() + l2_addr = getattr(master_account, 'l2_address', None) or l1_address + return { + "accountIndex": idx, + "l2Address": l2_addr, + "availableBalance": getattr(account_details, "available_balance", "0") + } + await api_client.close() + return None + except Exception: + return None + + +async def wait_for_account(l1_address: str) -> Optional[Dict]: + """Wait for account to be created after deposit.""" + for i in range(60): + account_info = await get_account(l1_address) + if account_info: + return account_info + if i % 6 == 0: + print(f"Waiting... ({i * 10 // 60}m)") + await asyncio.sleep(10) + return None + + +def deposit_direct(deposit_to_address: str) -> bool: + """ + Direct deposit via Ethereum using deposit(address _to, uint16 _assetIndex, uint8 _routeType, uint256 _amount). + Method ID: 0x8a857083 + """ + account = Account.from_key(ETH_PRIVATE_KEY) + wallet_address = to_checksum_address(account.address) + deposit_to_address = to_checksum_address(deposit_to_address) + usdc_address = to_checksum_address(USDC_CONTRACT) + zklighter_address = to_checksum_address(ZKLIGHTER_CONTRACT) + + decimals_selector = '0x313ce567' + resp = requests.post(L1_RPC_URL, json={ + "jsonrpc": "2.0", + "method": "eth_call", + "params": [{"to": usdc_address, "data": decimals_selector}, "latest"], + "id": 1 + }) + decimals = int(resp.json()["result"], 16) + amount_in_units = int(DEPOSIT_AMOUNT * (10 ** decimals)) + + balance_selector = '0x70a08231' + balance_data = balance_selector + wallet_address[2:].rjust(64, '0') + resp = requests.post(L1_RPC_URL, json={ + "jsonrpc": "2.0", + "method": "eth_call", + "params": [{"to": usdc_address, "data": balance_data}, "latest"], + "id": 1 + }) + balance = int(resp.json()["result"], 16) + if balance < amount_in_units: + print(f"❌ Insufficient balance. Required: {DEPOSIT_AMOUNT} USDC") + return False + + allowance_selector = '0xdd62ed3e' + allowance_data = allowance_selector + encode(['address', 'address'], [wallet_address, zklighter_address]).hex() + resp = requests.post(L1_RPC_URL, json={ + "jsonrpc": "2.0", + "method": "eth_call", + "params": [{"to": usdc_address, "data": allowance_data}, "latest"], + "id": 1 + }) + allowance = int(resp.json()["result"], 16) + + resp = requests.post(L1_RPC_URL, json={ + "jsonrpc": "2.0", + "method": "eth_getTransactionCount", + "params": [wallet_address, "latest"], + "id": 1 + }) + nonce = int(resp.json()["result"], 16) + + resp = requests.post(L1_RPC_URL, json={ + "jsonrpc": "2.0", + "method": "eth_gasPrice", + "params": [], + "id": 1 + }) + gas_price = int(resp.json()["result"], 16) + + if allowance < amount_in_units: + approve_selector = '0x095ea7b3' + approve_data = approve_selector + encode(['address', 'uint256'], [zklighter_address, amount_in_units]).hex() + + tx = {"to": usdc_address, "value": 0, "gas": 100000, "gasPrice": gas_price, "nonce": nonce, "chainId": 1, "data": approve_data} + signed = account.sign_transaction(tx) + raw_tx = '0x' + signed.raw_transaction.hex() + tx_hash = requests.post(L1_RPC_URL, json={ + "jsonrpc": "2.0", + "method": "eth_sendRawTransaction", + "params": [raw_tx], + "id": 1 + }).json().get("result") + + if not tx_hash: + print("❌ Approval failed") + return False + + # Wait for approval confirmation + for _ in range(30): + receipt = requests.post(L1_RPC_URL, json={ + "jsonrpc": "2.0", + "method": "eth_getTransactionReceipt", + "params": [tx_hash], + "id": 1 + }).json().get("result") + if receipt and receipt.get("status") == "0x1": + break + time.sleep(2) + + resp = requests.post(L1_RPC_URL, json={ + "jsonrpc": "2.0", + "method": "eth_getTransactionCount", + "params": [wallet_address, "latest"], + "id": 1 + }) + nonce = int(resp.json()["result"], 16) + + deposit_selector = '0x8a857083' + deposit_data = deposit_selector + encode( + ['address', 'uint16', 'uint8', 'uint256'], + [deposit_to_address, ASSET_INDEX_USDC, ROUTE_TYPE_PERP, amount_in_units] + ).hex() + + tx = {"to": zklighter_address, "value": 0, "gas": 200000, "gasPrice": gas_price, "nonce": nonce, "chainId": 1, "data": deposit_data} + signed = account.sign_transaction(tx) + raw_tx = '0x' + signed.raw_transaction.hex() + + tx_hash = requests.post(L1_RPC_URL, json={ + "jsonrpc": "2.0", + "method": "eth_sendRawTransaction", + "params": [raw_tx], + "id": 1 + }).json().get("result") + + if not tx_hash: + print("❌ Deposit failed") + return False + + print(f"✅ Deposit tx: {tx_hash}") + return True + + +async def generate_api_keys(account_index: int) -> Tuple[Dict[int, str], Dict[int, str]]: + """Generate API key pairs.""" + private_keys: Dict[int, str] = {} + public_keys: Dict[int, str] = {} + + for i in range(NUM_API_KEYS): + idx = API_KEY_INDEX + i + private_key, public_key, err = lighter.create_api_key() + if err is not None: + raise Exception(f"Failed to generate API key: {err}") + private_keys[idx] = private_key + public_keys[idx] = public_key + + return private_keys, public_keys + + +async def register_api_keys(account_index: int, private_keys: Dict[int, str], public_keys: Dict[int, str]) -> bool: + """Register API keys on the exchange.""" + try: + tx_client = lighter.SignerClient( + url=BASE_URL, + account_index=account_index, + api_private_keys=private_keys, + ) + + for idx, pub in public_keys.items(): + _, err = await tx_client.change_api_key( + eth_private_key=ETH_PRIVATE_KEY, + new_pubkey=pub, + api_key_index=idx, + ) + if err is not None: + await tx_client.close() + return False + + await asyncio.sleep(10) + err = tx_client.check_client() + await tx_client.close() + return err is None + except Exception as e: + print(f"Failed to register API keys: {e}") + return False + + +def save_config(account_index: str, l1_address: str, l2_address: str, private_keys: Dict[int, str]): + """Save configuration to JSON file.""" + private_keys_dict = {str(k): v for k, v in private_keys.items()} + save_api_key_config(BASE_URL, account_index, private_keys_dict, OUTPUT_FILE) + try: + with open(OUTPUT_FILE, "r", encoding="utf-8") as f: + config = json.load(f) + config["l1_address"] = l1_address + config["l2_address"] = l2_address + config["created_at"] = time.strftime("%Y-%m-%dT%H:%M:%S") + with open(OUTPUT_FILE, "w", encoding="utf-8") as f: + json.dump(config, f, indent=2, ensure_ascii=False) + except Exception: + config = { + "base_url": BASE_URL, + "account_index": account_index, + "private_keys": private_keys_dict, + "l1_address": l1_address, + "l2_address": l2_address, + "created_at": time.strftime("%Y-%m-%dT%H:%M:%S") + } + with open(OUTPUT_FILE, "w", encoding="utf-8") as f: + json.dump(config, f, indent=2, ensure_ascii=False) + + +async def main(): + if not ETH_PRIVATE_KEY or not L1_RPC_URL: + print("❌ ETH_PRIVATE_KEY and L1_RPC_URL required") + return + + account = Account.from_key(ETH_PRIVATE_KEY) + l1_address = to_checksum_address(account.address) + + account_info = await get_account(l1_address) + + if account_info: + balance = float(account_info['availableBalance']) + if balance > 0: + print(f"✅ Account found (Balance: {balance} USDC)") + else: + print("Depositing...") + deposit_address = account_info.get("l2Address") or l1_address + if not deposit_direct(deposit_address): + return + await asyncio.sleep(30) + account_info = await wait_for_account(l1_address) + if not account_info: + print("❌ Account not found after deposit") + return + else: + print("Depositing to create account...") + if not deposit_direct(l1_address): + return + account_info = await wait_for_account(l1_address) + if not account_info: + print("❌ Account not found after deposit") + return + + print(f"✅ Account: {account_info['accountIndex']}, Balance: {account_info['availableBalance']} USDC") + + private_keys, public_keys = await generate_api_keys(account_info["accountIndex"]) + await register_api_keys(account_info["accountIndex"], private_keys, public_keys) + + save_config(str(account_info["accountIndex"]), l1_address, account_info["l2Address"], private_keys) + + print(f"\n✅ Onboarding complete") + print(f" Config: {OUTPUT_FILE}") + + +if __name__ == "__main__": + asyncio.run(main()) + diff --git a/examples/onboard_indirect.py b/examples/onboard_indirect.py new file mode 100644 index 0000000..f23d4bb --- /dev/null +++ b/examples/onboard_indirect.py @@ -0,0 +1,334 @@ +import asyncio +import os +import json +import time +import lighter +import requests +from typing import Optional, Dict, Tuple +from eth_account import Account +from eth_utils import to_checksum_address +from eth_abi import encode +from utils import save_api_key_config + +# Supported chains and their USDC contracts +CHAIN_CONFIGS = { + 42161: {"name": "Arbitrum", "usdc": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831"}, + 8453: {"name": "Base", "usdc": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"}, + 43114: {"name": "Avalanche", "usdc": "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E"}, + 999: {"name": "HyperEVM", "usdc": None}, # Add USDC contract when available + 101: {"name": "Solana", "usdc": None}, # Solana uses different approach +} + +# Environment variables +BASE_URL = os.getenv("BASE_URL", "https://testnet.zklighter.elliot.ai") +ETH_PRIVATE_KEY = os.getenv("ETH_PRIVATE_KEY", "") +L2_RPC_URL = os.getenv("L2_RPC_URL", "") +CHAIN_ID = int(os.getenv("CHAIN_ID", "42161")) +USDC_CONTRACT = os.getenv("USDC_CONTRACT", "") +DEPOSIT_AMOUNT = float(os.getenv("DEPOSIT_AMOUNT", "6")) +OUTPUT_FILE = "api_key_config.json" +API_KEY_INDEX = int(os.getenv("API_KEY_INDEX", "4")) +NUM_API_KEYS = int(os.getenv("NUM_API_KEYS", "1")) + + +async def get_intent_address(l1_address: str, chain_id: int) -> Optional[str]: + """Get intent address for deposit.""" + params = { + "chain_id": str(chain_id), + "from_addr": l1_address, + "amount": "0", + "is_external_deposit": "true" + } + response = requests.post( + f"{BASE_URL}/api/v1/createIntentAddress", + data=params, + headers={"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8"} + ) + if response.status_code == 200: + data = response.json() + return data.get("intent_address") + return None + + +async def get_account(l1_address: str) -> Optional[Dict]: + """Get account info including balance.""" + try: + l1_address = to_checksum_address(l1_address) + api_client = lighter.ApiClient(configuration=lighter.Configuration(host=BASE_URL)) + account_api = lighter.AccountApi(api_client) + response = await account_api.accounts_by_l1_address(l1_address=l1_address) + if response.sub_accounts and len(response.sub_accounts) > 0: + accounts = response.sub_accounts + master_account = min(accounts, key=lambda x: int(x.index)) + idx = int(master_account.index) + if idx == 0: + await api_client.close() + return None + account_details = await account_api.account(by="index", value=str(idx)) + await api_client.close() + l2_addr = getattr(master_account, 'l2_address', None) or l1_address + return { + "accountIndex": idx, + "l2Address": l2_addr, + "availableBalance": getattr(account_details, "available_balance", "0") + } + await api_client.close() + return None + except Exception: + return None + + +async def wait_for_account(l1_address: str) -> Optional[Dict]: + """Wait for account to be created after deposit.""" + for i in range(60): + account_info = await get_account(l1_address) + if account_info: + return account_info + if i % 6 == 0: + print(f"Waiting... ({i * 10 // 60}m)") + await asyncio.sleep(10) + return None + + +async def get_usdc_balance(wallet_address: str, usdc_address: str) -> float: + """Get USDC balance on the chain.""" + decimals_selector = '0x313ce567' + balance_selector = '0x70a08231' + + resp = requests.post(L2_RPC_URL, json={ + "jsonrpc": "2.0", + "method": "eth_call", + "params": [{"to": usdc_address, "data": decimals_selector}, "latest"], + "id": 1 + }) + decimals = int(resp.json()["result"], 16) + + data = balance_selector + wallet_address[2:].rjust(64, '0') + resp = requests.post(L2_RPC_URL, json={ + "jsonrpc": "2.0", + "method": "eth_call", + "params": [{"to": usdc_address, "data": data}, "latest"], + "id": 1 + }) + balance = int(resp.json()["result"], 16) + return balance / (10 ** decimals) + + +async def deposit_indirect(l1_address: str) -> bool: + """Deposit USDC to intent address on external chain.""" + intent_addr = await get_intent_address(l1_address, CHAIN_ID) + if not intent_addr: + print("❌ Failed to get intent address") + return False + + chain_config = CHAIN_CONFIGS.get(CHAIN_ID, {"name": f"Chain {CHAIN_ID}", "usdc": None}) + chain_name = chain_config["name"] + usdc_address = to_checksum_address(USDC_CONTRACT or chain_config.get("usdc")) + + if not usdc_address: + print(f"❌ USDC contract address required for {chain_name}") + print(" Set USDC_CONTRACT environment variable") + return False + + print(f"Intent Address ({chain_name}): {intent_addr}") + + account = Account.from_key(ETH_PRIVATE_KEY) + wallet_address = to_checksum_address(account.address) + intent_addr = to_checksum_address(intent_addr) + + # Check balance + balance = await get_usdc_balance(wallet_address, usdc_address) + if balance < DEPOSIT_AMOUNT: + print(f"❌ Insufficient balance. Required: {DEPOSIT_AMOUNT}, Available: {balance}") + return False + + decimals_selector = '0x313ce567' + resp = requests.post(L2_RPC_URL, json={ + "jsonrpc": "2.0", + "method": "eth_call", + "params": [{"to": usdc_address, "data": decimals_selector}, "latest"], + "id": 1 + }) + decimals = int(resp.json()["result"], 16) + amount_in_units = int(DEPOSIT_AMOUNT * (10 ** decimals)) + + transfer_selector = '0xa9059cbb' + transfer_data = transfer_selector + encode(['address', 'uint256'], [intent_addr, amount_in_units]).hex() + + nonce = int(requests.post(L2_RPC_URL, json={ + "jsonrpc": "2.0", + "method": "eth_getTransactionCount", + "params": [wallet_address, "latest"], + "id": 1 + }).json()["result"], 16) + + gas_price = int(requests.post(L2_RPC_URL, json={ + "jsonrpc": "2.0", + "method": "eth_gasPrice", + "params": [], + "id": 1 + }).json()["result"], 16) + + tx = { + "to": usdc_address, + "value": 0, + "gas": 100000, + "gasPrice": gas_price, + "nonce": nonce, + "chainId": CHAIN_ID, + "data": transfer_data + } + signed = account.sign_transaction(tx) + raw_tx = '0x' + signed.raw_transaction.hex() + + tx_hash = requests.post(L2_RPC_URL, json={ + "jsonrpc": "2.0", + "method": "eth_sendRawTransaction", + "params": [raw_tx], + "id": 1 + }).json().get("result") + + if not tx_hash: + print("❌ Transfer failed") + return False + + print(f"✅ Transfer tx: {tx_hash}") + + for _ in range(30): + receipt = requests.post(L2_RPC_URL, json={ + "jsonrpc": "2.0", + "method": "eth_getTransactionReceipt", + "params": [tx_hash], + "id": 1 + }).json().get("result") + if receipt and receipt.get("status") == "0x1": + return True + await asyncio.sleep(2) + + return False + + +async def generate_api_keys(account_index: int) -> Tuple[Dict[int, str], Dict[int, str]]: + """Generate API key pairs.""" + private_keys: Dict[int, str] = {} + public_keys: Dict[int, str] = {} + + for i in range(NUM_API_KEYS): + idx = API_KEY_INDEX + i + private_key, public_key, err = lighter.create_api_key() + if err is not None: + raise Exception(f"Failed to generate API key: {err}") + private_keys[idx] = private_key + public_keys[idx] = public_key + + return private_keys, public_keys + + +async def register_api_keys(account_index: int, private_keys: Dict[int, str], public_keys: Dict[int, str]) -> bool: + """Register API keys on the exchange.""" + try: + tx_client = lighter.SignerClient( + url=BASE_URL, + account_index=account_index, + api_private_keys=private_keys, + ) + + for idx, pub in public_keys.items(): + _, err = await tx_client.change_api_key( + eth_private_key=ETH_PRIVATE_KEY, + new_pubkey=pub, + api_key_index=idx, + ) + if err is not None: + await tx_client.close() + return False + + await asyncio.sleep(10) + err = tx_client.check_client() + await tx_client.close() + return err is None + except Exception as e: + print(f"Failed to register API keys: {e}") + return False + + +def save_config(account_index: str, l1_address: str, l2_address: str, private_keys: Dict[int, str]): + """Save configuration to JSON file.""" + private_keys_dict = {str(k): v for k, v in private_keys.items()} + save_api_key_config(BASE_URL, account_index, private_keys_dict, OUTPUT_FILE) + try: + with open(OUTPUT_FILE, "r", encoding="utf-8") as f: + config = json.load(f) + config["l1_address"] = l1_address + config["l2_address"] = l2_address + config["created_at"] = time.strftime("%Y-%m-%dT%H:%M:%S") + with open(OUTPUT_FILE, "w", encoding="utf-8") as f: + json.dump(config, f, indent=2, ensure_ascii=False) + except Exception: + config = { + "base_url": BASE_URL, + "account_index": account_index, + "private_keys": private_keys_dict, + "l1_address": l1_address, + "l2_address": l2_address, + "created_at": time.strftime("%Y-%m-%dT%H:%M:%S") + } + with open(OUTPUT_FILE, "w", encoding="utf-8") as f: + json.dump(config, f, indent=2, ensure_ascii=False) + + +async def main(): + if not ETH_PRIVATE_KEY or not L2_RPC_URL: + print("❌ ETH_PRIVATE_KEY and L2_RPC_URL required") + return + + chain_config = CHAIN_CONFIGS.get(CHAIN_ID, {"name": f"Chain {CHAIN_ID}"}) + chain_name = chain_config["name"] + print(f"Using {chain_name} (Chain ID: {CHAIN_ID})") + + if not USDC_CONTRACT and not chain_config.get("usdc"): + print(f"❌ USDC_CONTRACT required for {chain_name}") + print(" Set USDC_CONTRACT environment variable") + return + + account = Account.from_key(ETH_PRIVATE_KEY) + l1_address = to_checksum_address(account.address) + + account_info = await get_account(l1_address) + + if account_info: + balance = float(account_info['availableBalance']) + if balance > 0: + print(f"✅ Account found (Balance: {balance} USDC)") + else: + print("Depositing...") + if not await deposit_indirect(l1_address): + return + await asyncio.sleep(30) + account_info = await wait_for_account(l1_address) + if not account_info: + print("❌ Account not found after deposit") + return + else: + print("Depositing to create account...") + if not await deposit_indirect(l1_address): + return + account_info = await wait_for_account(l1_address) + if not account_info: + print("❌ Account not found after deposit") + return + + print(f"✅ Account: {account_info['accountIndex']}, Balance: {account_info['availableBalance']} USDC") + + private_keys, public_keys = await generate_api_keys(account_info["accountIndex"]) + await register_api_keys(account_info["accountIndex"], private_keys, public_keys) + + save_config(str(account_info["accountIndex"]), l1_address, account_info["l2Address"], private_keys) + + print(f"\n✅ Onboarding complete") + print(f" Config: {OUTPUT_FILE}") + + +if __name__ == "__main__": + asyncio.run(main()) + diff --git a/examples/read-only-auth/generate.py b/examples/read-only-auth/generate.py index bd5c407..cc5041f 100644 --- a/examples/read-only-auth/generate.py +++ b/examples/read-only-auth/generate.py @@ -1,13 +1,10 @@ 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) @@ -18,16 +15,20 @@ def create_auth_token_for_timestamp(signer_client, timestamp, expiry_hours): 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}") + + if "api_key_private_keys" in account_info: + api_key_private_keys = account_info["api_key_private_keys"] + api_key_indices = account_info.get("api_key_indices", list(api_key_private_keys.keys())) + api_key_index = int(api_key_indices[0]) if api_key_indices else int(list(api_key_private_keys.keys())[0]) + api_key_private_key = api_key_private_keys[str(api_key_index)] + else: + api_key_private_key = account_info["api_key_private_key"] + api_key_index = int(account_info["api_key_index"]) signer_client = lighter.SignerClient( url=base_url, - private_key=api_key_private_key, account_index=account_index, - api_key_index=api_key_index, + api_private_keys={api_key_index: api_key_private_key}, ) current_time = int(time.time()) @@ -43,9 +44,8 @@ async def generate_tokens_for_account(account_info, base_url, duration_days): 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}") + print(f"Failed to generate token for timestamp {timestamp}: {e}") await signer_client.close() @@ -61,11 +61,11 @@ async def main(): 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") + print(f"error: config file '{config_file}' not found", file=sys.stderr) + print("error: run setup.py first: python3 setup.py > config.json", file=sys.stderr) sys.exit(1) except json.JSONDecodeError as e: - logging.error(f"Invalid JSON in config file: {e}") + print(f"error: invalid JSON in config file: {e}", file=sys.stderr) sys.exit(1) num_days = int(os.getenv("NUM_DAYS") or 28) @@ -74,16 +74,13 @@ async def main(): duration_days = config.get("DURATION_IN_DAYS", num_days) if not base_url: - logging.error("BASE_URL not found in config") + print("error: BASE_URL not found in config", file=sys.stderr) sys.exit(1) if not accounts: - logging.error("No accounts found in config") + print("error: no accounts found in config", file=sys.stderr) 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) @@ -93,10 +90,7 @@ async def main(): 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") + print(f"Successfully generated tokens and saved to {output_file}") if __name__ == "__main__": diff --git a/examples/read-only-auth/setup.py b/examples/read-only-auth/setup.py index 31790ad..7f74df5 100644 --- a/examples/read-only-auth/setup.py +++ b/examples/read-only-auth/setup.py @@ -1,111 +1,133 @@ 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: +import os + +BASE_URL = os.getenv("BASE_URL", "https://testnet.zklighter.elliot.ai") +ETH_PRIVATE_KEY = os.getenv("ETH_PRIVATE_KEY", "") +API_KEY_INDEX = int(os.getenv("API_KEY_INDEX", "253")) +NUM_API_KEYS = int(os.getenv("NUM_API_KEYS", "1")) + +async def setup_account(eth_private_key, account_index, base_url, api_key_index, num_keys): + """ + Setup API keys for a single account. + + Args: + eth_private_key: User's L1 Ethereum private key (for signing transactions) + account_index: Account index on the exchange + base_url: API base URL + api_key_index: Starting API key index + num_keys: Number of API keys to generate and register + + Returns: + Tuple of (config_dict, error_string) + """ + try: + private_keys = {} + public_keys = {} + + for i in range(num_keys): + idx = api_key_index + i + private_key, public_key, err = lighter.create_api_key() + if err is not None: + return None, f"Failed to create API key {idx}: {err}" + private_keys[idx] = private_key + public_keys[idx] = public_key + + tx_client = lighter.SignerClient( + url=base_url, + account_index=account_index, + api_private_keys=private_keys, + ) + + for idx, pub_key in public_keys.items(): + response, err = await tx_client.change_api_key( + eth_private_key=eth_private_key, + new_pubkey=pub_key, + api_key_index=idx, + ) + if err is not None: + await tx_client.close() + return None, f"Failed to register API key {idx}: {err}" + + await asyncio.sleep(5) + + err = tx_client.check_client() 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 + if err is not None: + return None, f"Failed to verify API keys: {err}" + + return { + "account_index": account_index, + "api_key_indices": list(private_keys.keys()), + "api_key_private_keys": private_keys, + }, None + + except Exception as e: + return None, f"Exception in setup_account: {str(e)}" async def main(): + if not ETH_PRIVATE_KEY or not BASE_URL: + print("Error: ETH_PRIVATE_KEY and BASE_URL environment variables are required") + return + 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) + 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) + 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, + int(sub_account.index), BASE_URL, API_KEY_INDEX, + NUM_API_KEYS, ) - + if err is not None: - logging.error(err) + print(f"error: failed to setup account {sub_account.index}: {err}", file=sys.stderr) else: accounts.append(result) - + if not accounts: - print("error: failed to setup any accounts", file=__import__('sys').stderr) + print("Error: failed to setup any accounts", file=__import__('sys').stderr) await api_client.close() return - + + config = { + "BASE_URL": BASE_URL, + "ACCOUNTS": accounts, + } + with open(config_file, "w", encoding="utf-8") as f: - json.dump({ - "BASE_URL": BASE_URL, - "ACCOUNTS": accounts, - }, f, ensure_ascii=False, indent=2) - + json.dump(config, f, ensure_ascii=False, indent=2) + await api_client.close()