/** * Shared utilities for NLA CLI */ import { foundry, sepolia, mainnet, baseSepolia } from "viem/chains"; import type { Chain } from "viem/chains"; import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs"; import { join, dirname } from "path"; import { homedir } from "os"; import { contractAddresses } from "alkahest-ts"; import { fileURLToPath } from "url"; // Get the directory of this utils file (dist/cli or cli/) const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); /** * Load .env file and set environment variables */ export function loadEnvFile(envPath: string): void { if (!existsSync(envPath)) { throw new Error(`.env file not found: ${envPath}`); } const content = readFileSync(envPath, "utf-8"); const lines = content.split("\n"); for (const line of lines) { const trimmed = line.trim(); // Skip empty lines and comments if (!trimmed || trimmed.startsWith("#")) { continue; } // Parse KEY=VALUE format const match = trimmed.match(/^([^=]+)=(.*)$/); if (match) { const key = match[1].trim(); let value = match[2].trim(); // Remove surrounding quotes if present if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { value = value.slice(1, -1); } // Only set if not already set if (!process.env[key]) { process.env[key] = value; } } } } /** * Get viem chain configuration from network name */ export function getChainFromNetwork(network: string): Chain { // Normalize the network name: lowercase and replace spaces with dashes const normalized = network.toLowerCase().replace(/\s+/g, '-'); switch (normalized) { case "localhost": case "anvil": return foundry; case "sepolia": case "ethereum-sepolia": return sepolia; case "base-sepolia": return baseSepolia; case "mainnet": case "ethereum": return mainnet; default: return foundry; } } /** * Get NLA config directory (~/.nla) */ export function getNLAConfigDir(): string { const configDir = join(homedir(), '.nla'); if (!existsSync(configDir)) { mkdirSync(configDir, { recursive: true }); } return configDir; } /** * Get current environment from config */ export function getCurrentEnvironment(): string { const configPath = join(getNLAConfigDir(), 'config.json'); if (!existsSync(configPath)) { // Default to anvil return 'anvil'; } try { const config = JSON.parse(readFileSync(configPath, 'utf-8')); return config.environment || 'anvil'; } catch (e) { return 'anvil'; } } /** * Set current environment in config */ export function setCurrentEnvironment(env: string): void { const configPath = join(getNLAConfigDir(), 'config.json'); const config = existsSync(configPath) ? JSON.parse(readFileSync(configPath, 'utf-8')) : {}; config.environment = env; writeFileSync(configPath, JSON.stringify(config, null, 2)); } /** * Get private key from config or environment */ export function getPrivateKey(): string | undefined { // First check environment variable if (process.env.PRIVATE_KEY) { return process.env.PRIVATE_KEY; } // Then check config file const configPath = join(getNLAConfigDir(), 'config.json'); if (!existsSync(configPath)) { return undefined; } try { const config = JSON.parse(readFileSync(configPath, 'utf-8')); return config.privateKey; } catch (e) { return undefined; } } /** * Set private key in config */ export function setPrivateKey(privateKey: string): void { const configPath = join(getNLAConfigDir(), 'config.json'); const config = existsSync(configPath) ? JSON.parse(readFileSync(configPath, 'utf-8')) : {}; config.privateKey = privateKey; writeFileSync(configPath, JSON.stringify(config, null, 2)); } /** * Clear private key from config */ export function clearPrivateKey(): void { const configPath = join(getNLAConfigDir(), 'config.json'); if (!existsSync(configPath)) { return; } try { const config = JSON.parse(readFileSync(configPath, 'utf-8')); delete config.privateKey; writeFileSync(configPath, JSON.stringify(config, null, 2)); } catch (e) { // Ignore errors } } /** * Get deployment path for environment * Automatically looks in the CLI directory where utils.ts is located * @param env - The environment name (anvil, sepolia, etc.) */ export function getDeploymentPath(env?: string): string { const environment = env || getCurrentEnvironment(); const filename = `${environment}.json`; // When deployed via npm, utils.js will be in dist/cli/ // and deployment files will be in dist/cli/deployments/ const deploymentPath = join(__dirname, 'deployments', filename); return deploymentPath; } /** * Load deployment file and fill empty addresses with defaults from contractAddresses * If deploymentFilePath doesn't exist, tries to load deployment for current network * @param deploymentFilePath - Optional path to deployment file */ export function loadDeploymentWithDefaults(deploymentFilePath?: string): { network: string; chainId: number; rpcUrl: string; addresses: Record; } { let actualPath = deploymentFilePath; // If no path provided or path doesn't exist, try current network if (!actualPath || !existsSync(actualPath)) { const currentEnv = getCurrentEnvironment(); const autoPath = getDeploymentPath(currentEnv); if (existsSync(autoPath)) { actualPath = autoPath; } else if (!actualPath) { throw new Error(`No deployment file found for current environment: ${currentEnv}. Try running from the project directory or use --deployment `); } else { throw new Error(`Deployment file not found: ${actualPath}`); } } const content = readFileSync(actualPath, "utf-8"); const deployment = JSON.parse(content); let finalAddresses: Record = {}; // Start with default addresses from contractAddresses if available // contractAddresses is indexed by chain name (e.g., "Base Sepolia", "foundry") const chainName = deployment.network; if (contractAddresses[chainName]) { finalAddresses = { ...contractAddresses[chainName] }; } // Override with deployment addresses, but only if they're not empty strings if (deployment.addresses && Object.keys(deployment.addresses).length > 0) { for (const [key, value] of Object.entries(deployment.addresses)) { if (value && value !== "") { finalAddresses[key] = value as string; } } } return { network: deployment.network, chainId: deployment.chainId, rpcUrl: deployment.rpcUrl, addresses: finalAddresses, }; }