import { readValidatedRecord, type StorageAdapter } from '@lifi/perps-sdk' import type { LighterProviderKey } from '@lifi/perps-types' import type { Address } from 'viem' import { LIGHTER_PROVIDER_KEY } from '../constants.js' // The private key here is a Lighter custom keypair — generated via WASM // GenerateAPIKey — not an Ethereum private key. The storage key is namespaced // by the resolved provider instance key so two Lighter instances sharing a // storage adapter (e.g. `lighter` and `lighter-rh`) never clobber each other's // API key. const STORAGE_PREFIX = 'lifi-perps-lighter-key' /** * Persisted Lighter API-key material associated with one L2 account. * `apiKeyPrivateKey` is a Lighter-native signing key, not an Ethereum key; * `apiKeyIndex` identifies its registered on-chain slot. * * @public */ export interface LighterApiKey { /** Lighter account index, looked up once via accountsByL1Address. */ accountIndex: number /** API key slot (0-255), as named by the backend registration payload. */ apiKeyIndex: number /** Lighter-native private key (0x-prefixed hex). */ apiKeyPrivateKey: string /** Corresponding public key, registered via ChangePubKey. */ apiKeyPublicKey: string } const isLighterApiKey = (value: unknown): value is LighterApiKey => { if (typeof value !== 'object' || value === null) { return false } const { accountIndex, apiKeyIndex, apiKeyPrivateKey, apiKeyPublicKey } = value as Record return ( typeof accountIndex === 'number' && Number.isFinite(accountIndex) && typeof apiKeyIndex === 'number' && Number.isFinite(apiKeyIndex) && typeof apiKeyPrivateKey === 'string' && apiKeyPrivateKey.length > 0 && typeof apiKeyPublicKey === 'string' && apiKeyPublicKey.length > 0 ) } /** * Storage-backed cache for Lighter API keys. Records are namespaced by L1 * address and provider instance so multiple Lighter deployments can share one * {@link StorageAdapter} without collisions. * * @public */ export class LighterKeyStore { private readonly storage: StorageAdapter private readonly cache = new Map() private providerKey: LighterProviderKey /** * Create a key store using `storage` and the optional provider namespace. * The default provider key preserves the package's standard Lighter storage * namespace. */ constructor( storage: StorageAdapter, providerKey: LighterProviderKey = LIGHTER_PROVIDER_KEY ) { this.storage = storage this.providerKey = providerKey } /** * Bind the resolved provider instance key. {@link LighterProvider} calls this * during registration so a consumer-constructed keystore adopts the plugin * instance's identity without pre-namespacing the adapter. * @internal */ bindProviderKey(providerKey: LighterProviderKey): void { this.providerKey = providerKey } // The default instance keeps the legacy, un-namespaced key so existing // `lighter` users are not orphaned; only additional instances get a segment. private storageKey(address: Address): string { const lower = address.toLowerCase() return this.providerKey === LIGHTER_PROVIDER_KEY ? `${STORAGE_PREFIX}:${lower}` : `${STORAGE_PREFIX}:${this.providerKey}:${lower}` } /** * Load the API key for an L1 address, or `null` when no valid record exists. * Invalid persisted records are ignored by the storage validation boundary. */ async get(address: Address): Promise { const key = this.storageKey(address) const cached = this.cache.get(key) if (cached) { return cached } const parsed = await readValidatedRecord(this.storage, key, isLighterApiKey) if (!parsed) { return null } this.cache.set(key, parsed) return parsed } /** * Persist an API key for an L1 address, replacing any existing record under * that provider namespace. */ async set(address: Address, value: LighterApiKey): Promise { const key = this.storageKey(address) this.cache.set(key, value) await this.storage.set(key, JSON.stringify(value)) } /** * Remove the persisted API key for an L1 address and clear its in-memory * cache entry. */ async remove(address: Address): Promise { const key = this.storageKey(address) this.cache.delete(key) await this.storage.remove(key) } }