natural-language-agreements/cli/utils.ts

248 lines
7.2 KiB
TypeScript

/**
* 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<string, string>;
} {
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 <file>`);
} else {
throw new Error(`Deployment file not found: ${actualPath}`);
}
}
const content = readFileSync(actualPath, "utf-8");
const deployment = JSON.parse(content);
let finalAddresses: Record<string, string> = {};
// 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,
};
}