249 lines
7.0 KiB
TypeScript
249 lines
7.0 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 } from "path";
|
|
import { homedir } from "os";
|
|
import { contractAddresses } from "alkahest-ts";
|
|
|
|
/**
|
|
* 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 "devnet":
|
|
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 devnet
|
|
return 'devnet';
|
|
}
|
|
|
|
try {
|
|
const config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
return config.environment || 'devnet';
|
|
} catch (e) {
|
|
return 'devnet';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
export function getDeploymentPath(cliDir: string, env?: string): string {
|
|
const environment = env || getCurrentEnvironment();
|
|
const filename = `${environment}.json`;
|
|
|
|
// Try multiple locations
|
|
const paths = [
|
|
join(cliDir, 'deployments', filename), // dist/cli/deployments/
|
|
join(process.cwd(), 'cli', 'deployments', filename), // Project root
|
|
];
|
|
|
|
for (const path of paths) {
|
|
if (existsSync(path)) {
|
|
return path;
|
|
}
|
|
}
|
|
|
|
// Return the first path as default (even if it doesn't exist yet)
|
|
return paths[0];
|
|
}
|
|
|
|
/**
|
|
* Load deployment file and fill empty addresses with defaults from contractAddresses
|
|
* If deploymentFilePath doesn't exist, tries to load deployment for current network
|
|
*/
|
|
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(process.cwd(), currentEnv);
|
|
|
|
if (existsSync(autoPath)) {
|
|
actualPath = autoPath;
|
|
} else if (!actualPath) {
|
|
throw new Error(`No deployment file found for current environment: ${currentEnv}`);
|
|
} 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,
|
|
};
|
|
}
|