diff --git a/bun.lock b/bun.lock index 852a509..684ca24 100644 --- a/bun.lock +++ b/bun.lock @@ -11,13 +11,15 @@ "@perplexity-ai/ai-sdk": "^0.1.2", "@viem/anvil": "^0.0.10", "ai": "^6.0.5", - "alkahest-ts": "github:arkhai-io/alkahest", + "alkahest-ts": "^0.6.1", "arktype": "^2.1.23", "viem": "^2.42.1", "zod": "^3.25.76", }, "devDependencies": { "@types/bun": "latest", + "@types/node": "^20.0.0", + "typescript": "^5.9.3", }, "peerDependencies": { "typescript": "^5.9.3", @@ -65,7 +67,7 @@ "@types/bun": ["@types/bun@1.3.4", "", { "dependencies": { "bun-types": "1.3.4" } }, "sha512-EEPTKXHP+zKGPkhRLv+HI0UEX8/o+65hqARxLy8Ov5rIxMBPNTjeZww00CIihrIQGEQBYg+0roO5qOnS/7boGA=="], - "@types/node": ["@types/node@25.0.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-gWEkeiyYE4vqjON/+Obqcoeffmk0NF15WSBwSs7zwVA2bAbTaE0SJ7P0WNGoJn8uE7fiaV5a7dKYIJriEqOrmA=="], + "@types/node": ["@types/node@20.19.30", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g=="], "@vercel/oidc": ["@vercel/oidc@3.0.5", "", {}, "sha512-fnYhv671l+eTTp48gB4zEsTW/YtRgRPnkI2nT7x6qw5rkI1Lq2hTmQIpHPgyThI0znLK+vX2n9XxKdXZ7BUbbw=="], @@ -75,7 +77,7 @@ "ai": ["ai@6.0.5", "", { "dependencies": { "@ai-sdk/gateway": "3.0.4", "@ai-sdk/provider": "3.0.1", "@ai-sdk/provider-utils": "4.0.2", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CKL3dDHedWskC6EY67LrULonZBU9vL+Bwa+xQEcprBhJfxpogntG3utjiAkYuy5ZQatyWk+SmWG8HLvcnhvbRg=="], - "alkahest-ts": ["alkahest-ts@github:arkhai-io/alkahest#80a8273", { "dependencies": { "@viem/anvil": "^0.0.10", "arktype": "^2.1.23", "zod": "^3.25.76" }, "peerDependencies": { "typescript": "^5.9.3", "viem": "^2.38.3" } }, "arkhai-io-alkahest-80a8273"], + "alkahest-ts": ["alkahest-ts@0.6.1", "", { "dependencies": { "@viem/anvil": "^0.0.10", "arktype": "^2.1.23", "zod": "^3.25.76" }, "peerDependencies": { "typescript": "^5.9.3", "viem": "^2.38.3" } }, "sha512-0u1xUM9OLca6emKDVzn6ISx4VFg7TGZtA/7hnT3SOHOWVPLk5ruX12tbX5MO7hG1jxVHAO3wgIE2t7vl864/nQ=="], "arkregex": ["arkregex@0.0.5", "", { "dependencies": { "@ark/util": "0.56.0" } }, "sha512-ncYjBdLlh5/QnVsAA8De16Tc9EqmYM7y/WU9j+236KcyYNUXogpz3sC4ATIZYzzLxwI+0sEOaQLEmLmRleaEXw=="], @@ -133,7 +135,7 @@ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], "viem": ["viem@2.42.1", "", { "dependencies": { "@noble/curves": "1.9.1", "@noble/hashes": "1.8.0", "@scure/bip32": "1.7.0", "@scure/bip39": "1.6.0", "abitype": "1.1.0", "isows": "1.0.7", "ox": "0.9.6", "ws": "8.18.3" }, "peerDependencies": { "typescript": ">=5.0.4" }, "optionalPeers": ["typescript"] }, "sha512-NzT/f54jT+b0Um6pYzN/uAGMLg+3twhricAzXS+XH8pVIREzPEh7P25rlhPQnLYiPWzQd9mrFcvnm73Sc8bx+A=="], @@ -143,8 +145,12 @@ "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "bun-types/@types/node": ["@types/node@25.0.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-gWEkeiyYE4vqjON/+Obqcoeffmk0NF15WSBwSs7zwVA2bAbTaE0SJ7P0WNGoJn8uE7fiaV5a7dKYIJriEqOrmA=="], + "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], "ox/eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], + + "bun-types/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], } } diff --git a/cli/client/collect-escrow.ts b/cli/client/collect-escrow.ts index 5cad394..215c83c 100644 --- a/cli/client/collect-escrow.ts +++ b/cli/client/collect-escrow.ts @@ -6,46 +6,18 @@ */ import { parseArgs } from "util"; -import { createWalletClient, http, publicActions } from "viem"; +import { createWalletClient, http, publicActions, formatEther } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { existsSync, readFileSync } from "fs"; import { resolve, dirname, join } from "path"; import { fileURLToPath } from "url"; import { makeClient } from "alkahest-ts"; -import { getChainFromNetwork } from "../utils.js"; +import { getChainFromNetwork, loadDeploymentWithDefaults, getPrivateKey } from "../utils.js"; // Get the directory of the current module const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -// Helper function to find deployment file -function findDeploymentFile(deploymentPath: string): string | null { - // Try the provided path first - if (existsSync(resolve(deploymentPath))) { - return resolve(deploymentPath); - } - - // Try relative to current working directory - const cwdPath = resolve(process.cwd(), deploymentPath); - if (existsSync(cwdPath)) { - return cwdPath; - } - - // Try relative to the CLI installation directory - const cliPath = resolve(__dirname, "..", "deployments", "devnet.json"); - if (existsSync(cliPath)) { - return cliPath; - } - - // Try in the project root (for local development) - const projectPath = resolve(__dirname, "..", "..", "cli", "deployments", "devnet.json"); - if (existsSync(projectPath)) { - return projectPath; - } - - return null; -} - // Helper function to display usage function displayHelp() { console.log(` @@ -112,8 +84,8 @@ async function main() { // Get configuration const escrowUid = args["escrow-uid"]; const fulfillmentUid = args["fulfillment-uid"]; - const privateKey = args["private-key"] || process.env.PRIVATE_KEY; - const deploymentPath = args.deployment || "./cli/deployments/devnet.json"; + const privateKey = args["private-key"] || getPrivateKey(); + const deploymentPath = args.deployment; // Validate required parameters if (!escrowUid) { @@ -129,24 +101,25 @@ async function main() { } if (!privateKey) { - console.error("āŒ Error: Private key is required. Use --private-key or set PRIVATE_KEY"); - console.error("Run with --help for usage information."); + console.error("āŒ Error: Private key is required"); + console.error("\nšŸ’” You can either:"); + console.error(" 1. Set it globally: nla wallet:set --private-key "); + console.error(" 2. Use for this command only: --private-key "); + console.error(" 3. Set PRIVATE_KEY environment variable"); + console.error("\nRun with --help for usage information."); process.exit(1); } - // Load deployment file - const resolvedDeploymentPath = findDeploymentFile(deploymentPath); - if (!resolvedDeploymentPath) { - console.error(`āŒ Error: Deployment file not found: ${deploymentPath}`); + // Load deployment file (auto-detects current network if not specified) + let deployment; + try { + deployment = loadDeploymentWithDefaults(deploymentPath); + } catch (error) { + console.error(`āŒ Error: ${(error as Error).message}`); console.error("Please deploy contracts first or specify correct path with --deployment"); - console.error("\nSearched in:"); - console.error(` - ${resolve(deploymentPath)}`); - console.error(` - ${resolve(process.cwd(), deploymentPath)}`); - console.error(` - ${resolve(__dirname, "..", "deployments", "devnet.json")}`); process.exit(1); } - const deployment = JSON.parse(readFileSync(resolvedDeploymentPath, "utf-8")); const rpcUrl = args["rpc-url"] || deployment.rpcUrl; const chain = getChainFromNetwork(deployment.network); @@ -168,7 +141,7 @@ async function main() { // Check balance const balance = await walletClient.getBalance({ address: account.address }); - console.log(`šŸ’° ETH balance: ${parseFloat((balance / 10n ** 18n).toString()).toFixed(4)} ETH\n`); + console.log(`šŸ’° ETH balance: ${parseFloat(formatEther(balance)).toFixed(4)} ETH\n`); if (balance === 0n) { console.error("āŒ Error: Account has no ETH for gas. Please fund the account first."); diff --git a/cli/client/create-escrow.ts b/cli/client/create-escrow.ts index c705c11..1016eb6 100644 --- a/cli/client/create-escrow.ts +++ b/cli/client/create-escrow.ts @@ -7,7 +7,7 @@ */ import { parseArgs } from "util"; -import { createWalletClient, http, publicActions, parseEther } from "viem"; +import { createWalletClient, http, publicActions, parseEther, formatEther } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { existsSync, readFileSync } from "fs"; import { resolve, dirname, join } from "path"; @@ -15,48 +15,12 @@ import { fileURLToPath } from "url"; import { makeClient } from "alkahest-ts"; import { makeLLMClient } from "../.."; import {fixtures} from "alkahest-ts"; -import { getCurrentEnvironment } from "../commands/switch.js"; -import { getChainFromNetwork } from "../utils.js"; +import { getCurrentEnvironment, getChainFromNetwork, loadDeploymentWithDefaults, getPrivateKey } from "../utils.js"; // Get the directory of the current module const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -// Helper function to find deployment file -function findDeploymentFile(deploymentPath: string, environment?: string): string | null { - // If no path provided, use current environment - if (!deploymentPath) { - const env = environment || getCurrentEnvironment(); - deploymentPath = `./cli/deployments/${env}.json`; - } - - // Try the provided path first - if (existsSync(resolve(deploymentPath))) { - return resolve(deploymentPath); - } - - // Try relative to current working directory - const cwdPath = resolve(process.cwd(), deploymentPath); - if (existsSync(cwdPath)) { - return cwdPath; - } - - // Try relative to the CLI installation directory with current environment - const env = environment || getCurrentEnvironment(); - const cliPath = resolve(__dirname, "..", "deployments", `${env}.json`); - if (existsSync(cliPath)) { - return cliPath; - } - - // Try in the project root (for local development) - const projectPath = resolve(__dirname, "..", "..", "cli", "deployments", `${env}.json`); - if (existsSync(projectPath)) { - return projectPath; - } - - return null; -} - // Helper function to display usage function displayHelp() { const currentEnv = getCurrentEnvironment(); @@ -140,8 +104,8 @@ async function main() { const amount = args.amount; const tokenAddress = args.token; const oracleAddress = args.oracle; - const privateKey = args["private-key"] || process.env.PRIVATE_KEY; - const deploymentPath = args.deployment || `./cli/deployments/${getCurrentEnvironment()}.json`; + const privateKey = args["private-key"] || getPrivateKey(); + const deploymentPath = args.deployment; // Arbitration configuration with defaults const arbitrationProvider = args["arbitration-provider"] || "OpenAI"; @@ -179,24 +143,25 @@ Fulfillment: {{obligation}}`; } if (!privateKey) { - console.error("āŒ Error: Private key is required. Use --private-key or set PRIVATE_KEY"); - console.error("Run with --help for usage information."); + console.error("āŒ Error: Private key is required"); + console.error("\nšŸ’” You can either:"); + console.error(" 1. Set it globally: nla wallet:set --private-key "); + console.error(" 2. Use for this command only: --private-key "); + console.error(" 3. Set PRIVATE_KEY environment variable"); + console.error("\nRun with --help for usage information."); process.exit(1); } - // Load deployment file - const resolvedDeploymentPath = findDeploymentFile(deploymentPath); - if (!resolvedDeploymentPath) { - console.error(`āŒ Error: Deployment file not found: ${deploymentPath}`); + // Load deployment file (auto-detects current network if not specified) + let deployment; + try { + deployment = loadDeploymentWithDefaults(deploymentPath); + } catch (error) { + console.error(`āŒ Error: ${(error as Error).message}`); console.error("Please deploy contracts first or specify correct path with --deployment"); - console.error("\nSearched in:"); - console.error(` - ${resolve(deploymentPath)}`); - console.error(` - ${resolve(process.cwd(), deploymentPath)}`); - console.error(` - ${resolve(__dirname, "..", "deployments", "devnet.json")}`); process.exit(1); } - const deployment = JSON.parse(readFileSync(resolvedDeploymentPath, "utf-8")); const rpcUrl = args["rpc-url"] || deployment.rpcUrl; const chain = getChainFromNetwork(deployment.network); @@ -221,7 +186,7 @@ Fulfillment: {{obligation}}`; // Check balance const balance = await walletClient.getBalance({ address: account.address }); - console.log(`šŸ’° ETH balance: ${parseFloat((balance / 10n ** 18n).toString()).toFixed(4)} ETH\n`); + console.log(`šŸ’° ETH balance: ${parseFloat(formatEther(balance)).toFixed(4)} ETH\n`); if (balance === 0n) { console.error("āŒ Error: Account has no ETH for gas. Please fund the account first."); @@ -257,7 +222,7 @@ Fulfillment: {{obligation}}`; console.log("šŸ“‹ Creating escrow\n"); // Encode the demand with oracle arbiter - const arbiter = deployment.addresses.trustedOracleArbiter; + const arbiter = deployment.addresses.trustedOracleArbiter as `0x${string}`; const encodedDemand = client.arbiters.general.trustedOracle.encodeDemand({ oracle: oracleAddress as `0x${string}`, data: llmClient.llm.encodeDemand({ @@ -266,7 +231,7 @@ Fulfillment: {{obligation}}`; arbitrationPrompt, demand: demand }) - }); + }) as `0x${string}`; // Create the escrow const { attested: escrow } = await client.erc20.escrow.nonTierable.permitAndCreate( diff --git a/cli/client/fulfill-escrow.ts b/cli/client/fulfill-escrow.ts index 1b8e073..93a1968 100644 --- a/cli/client/fulfill-escrow.ts +++ b/cli/client/fulfill-escrow.ts @@ -7,47 +7,19 @@ */ import { parseArgs } from "util"; -import { createWalletClient, http, publicActions } from "viem"; +import { createWalletClient, http, publicActions, formatEther } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { existsSync, readFileSync } from "fs"; import { resolve, dirname, join } from "path"; import { fileURLToPath } from "url"; import { makeClient } from "alkahest-ts"; import { makeLLMClient } from "../.."; -import { getChainFromNetwork } from "../utils.js"; +import { getChainFromNetwork, loadDeploymentWithDefaults, getPrivateKey } from "../utils.js"; // Get the directory of the current module const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -// Helper function to find deployment file -function findDeploymentFile(deploymentPath: string): string | null { - // Try the provided path first - if (existsSync(resolve(deploymentPath))) { - return resolve(deploymentPath); - } - - // Try relative to current working directory - const cwdPath = resolve(process.cwd(), deploymentPath); - if (existsSync(cwdPath)) { - return cwdPath; - } - - // Try relative to the CLI installation directory - const cliPath = resolve(__dirname, "..", "deployments", "devnet.json"); - if (existsSync(cliPath)) { - return cliPath; - } - - // Try in the project root (for local development) - const projectPath = resolve(__dirname, "..", "..", "cli", "deployments", "devnet.json"); - if (existsSync(projectPath)) { - return projectPath; - } - - return null; -} - // Helper function to display usage function displayHelp() { console.log(` @@ -118,8 +90,8 @@ async function main() { const escrowUid = args["escrow-uid"]; const fulfillment = args.fulfillment; const oracleAddress = args.oracle; - const privateKey = args["private-key"] || process.env.PRIVATE_KEY; - const deploymentPath = args.deployment || "./cli/deployments/devnet.json"; + const privateKey = args["private-key"] || getPrivateKey(); + const deploymentPath = args.deployment ; // Validate required parameters if (!escrowUid) { @@ -141,24 +113,25 @@ async function main() { } if (!privateKey) { - console.error("āŒ Error: Private key is required. Use --private-key or set PRIVATE_KEY"); - console.error("Run with --help for usage information."); + console.error("āŒ Error: Private key is required"); + console.error("\nšŸ’” You can either:"); + console.error(" 1. Set it globally: nla wallet:set --private-key "); + console.error(" 2. Use for this command only: --private-key "); + console.error(" 3. Set PRIVATE_KEY environment variable"); + console.error("\nRun with --help for usage information."); process.exit(1); } - // Load deployment file - const resolvedDeploymentPath = findDeploymentFile(deploymentPath); - if (!resolvedDeploymentPath) { - console.error(`āŒ Error: Deployment file not found: ${deploymentPath}`); + // Load deployment file (auto-detects current network if not specified) + let deployment; + try { + deployment = loadDeploymentWithDefaults(deploymentPath); + } catch (error) { + console.error(`āŒ Error: ${(error as Error).message}`); console.error("Please deploy contracts first or specify correct path with --deployment"); - console.error("\nSearched in:"); - console.error(` - ${resolve(deploymentPath)}`); - console.error(` - ${resolve(process.cwd(), deploymentPath)}`); - console.error(` - ${resolve(__dirname, "..", "deployments", "devnet.json")}`); process.exit(1); } - const deployment = JSON.parse(readFileSync(resolvedDeploymentPath, "utf-8")); const rpcUrl = args["rpc-url"] || deployment.rpcUrl; const chain = getChainFromNetwork(deployment.network); @@ -182,7 +155,7 @@ async function main() { // Check balance const balance = await walletClient.getBalance({ address: account.address }); - console.log(`šŸ’° ETH balance: ${parseFloat((balance / 10n ** 18n).toString()).toFixed(4)} ETH\n`); + console.log(`šŸ’° ETH balance: ${parseFloat(formatEther(balance)).toFixed(4)} ETH\n`); if (balance === 0n) { console.error("āŒ Error: Account has no ETH for gas. Please fund the account first."); diff --git a/cli/commands/dev.ts b/cli/commands/dev.ts index 0df804f..10ddd8b 100644 --- a/cli/commands/dev.ts +++ b/cli/commands/dev.ts @@ -1,6 +1,7 @@ import { spawn, spawnSync } from "child_process"; import { existsSync, readFileSync, writeFileSync, createWriteStream, unlinkSync } from "fs"; import { join } from "path"; +import { getCurrentEnvironment, setCurrentEnvironment } from "../utils.js"; // Colors for console output const colors = { @@ -71,6 +72,16 @@ export async function runDevCommand(cliDir: string, envPath?: string) { console.log(`${colors.blue} Natural Language Agreement Oracle - Quick Setup${colors.reset}`); console.log(`${colors.blue}════════════════════════════════════════════════════════${colors.reset}\n`); + // Auto-switch to devnet environment + const currentEnv = getCurrentEnvironment(); + if (currentEnv !== 'devnet') { + console.log(`${colors.yellow}šŸ”„ Switching environment from ${currentEnv} to devnet...${colors.reset}`); + setCurrentEnvironment('devnet'); + console.log(`${colors.green}āœ… Switched to devnet${colors.reset}\n`); + } else { + console.log(`${colors.green}āœ… Already on devnet environment${colors.reset}\n`); + } + // Load .env file first loadEnvFile(envPath); console.log(''); diff --git a/cli/commands/switch.ts b/cli/commands/switch.ts index 6ae360d..c5755d5 100644 --- a/cli/commands/switch.ts +++ b/cli/commands/switch.ts @@ -2,6 +2,12 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs"; import { join, dirname } from "path"; import { homedir } from "os"; import { fileURLToPath } from "url"; +import { + getNLAConfigDir, + getCurrentEnvironment, + setCurrentEnvironment, + getDeploymentPath +} from "../utils.js"; // Get the directory of the current module const __filename = fileURLToPath(import.meta.url); @@ -16,65 +22,6 @@ const colors = { reset: '\x1b[0m' }; -// Get NLA config directory -function getNLAConfigDir(): string { - const configDir = join(homedir(), '.nla'); - if (!existsSync(configDir)) { - mkdirSync(configDir, { recursive: true }); - } - return configDir; -} - -// Get current environment -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 -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 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(__dirname, '..', 'deployments', filename), // Relative to switch.ts - 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]; -} - // Switch command export function runSwitchCommand(env?: string) { console.log(`${colors.blue}════════════════════════════════════════════════════════${colors.reset}`); @@ -86,19 +33,20 @@ export function runSwitchCommand(env?: string) { const current = getCurrentEnvironment(); console.log(`${colors.blue}Current environment:${colors.reset} ${colors.green}${current}${colors.reset}\n`); console.log('Available environments:'); - console.log(' • devnet (local Anvil blockchain)'); - console.log(' • sepolia (Ethereum Sepolia testnet)'); - console.log(' • mainnet (Ethereum mainnet)\n'); + console.log(' • devnet (local Anvil blockchain)'); + console.log(' • sepolia (Ethereum Sepolia testnet)'); + console.log(' • base-sepolia (Base Sepolia testnet)'); + console.log(' • mainnet (Ethereum mainnet)\n'); console.log(`${colors.yellow}Usage:${colors.reset} nla switch `); console.log(`${colors.yellow}Example:${colors.reset} nla switch sepolia\n`); return; } // Validate environment - const validEnvs = ['devnet', 'sepolia', 'mainnet']; + const validEnvs = ['devnet', 'sepolia', 'base-sepolia', 'mainnet']; if (!validEnvs.includes(env)) { console.error(`${colors.red}āŒ Invalid environment: ${env}${colors.reset}`); - console.log('Valid environments: devnet, sepolia, mainnet\n'); + console.log('Valid environments: devnet, sepolia, base-sepolia, mainnet\n'); process.exit(1); } @@ -120,6 +68,9 @@ export function runSwitchCommand(env?: string) { } else if (env === 'sepolia') { console.log('šŸ“ Using Ethereum Sepolia testnet'); console.log(' Make sure you have deployed contracts and updated sepolia.json\n'); + } else if (env === 'base-sepolia') { + console.log('šŸ“ Using Base Sepolia testnet'); + console.log(' Make sure you have deployed contracts and updated base-sepolia.json\n'); } else if (env === 'mainnet') { console.log('šŸ“ Using Ethereum mainnet'); console.log(` ${colors.yellow}āš ļø WARNING: This is production! Use with caution.${colors.reset}\n`); diff --git a/cli/commands/wallet.ts b/cli/commands/wallet.ts new file mode 100644 index 0000000..3108eeb --- /dev/null +++ b/cli/commands/wallet.ts @@ -0,0 +1,72 @@ +/** + * Wallet management commands + */ + +import { setPrivateKey, clearPrivateKey, getPrivateKey } from "../utils.js"; +import { privateKeyToAddress } from "viem/accounts"; + +/** + * Set wallet private key + */ +export async function setWallet(privateKey: string): Promise { + // Validate private key format + if (!privateKey.startsWith('0x')) { + console.error('āŒ Private key must start with 0x'); + process.exit(1); + } + + if (privateKey.length !== 66) { + console.error('āŒ Invalid private key length. Expected 66 characters (0x + 64 hex chars)'); + process.exit(1); + } + + try { + // Validate by deriving address + const address = privateKeyToAddress(privateKey as `0x${string}`); + + // Store in config + setPrivateKey(privateKey); + + console.log('āœ… Wallet configured successfully'); + console.log(`šŸ“ Address: ${address}`); + console.log('\nšŸ’” Your private key is stored in ~/.nla/config.json'); + console.log(' It will be used automatically for all transactions'); + } catch (error) { + console.error('āŒ Invalid private key format'); + process.exit(1); + } +} + +/** + * Show current wallet address + */ +export async function showWallet(): Promise { + const privateKey = getPrivateKey(); + + if (!privateKey) { + console.log('ā„¹ļø No wallet configured'); + console.log('\nšŸ’” Set your wallet with:'); + console.log(' nla wallet:set --private-key '); + return; + } + + try { + const address = privateKeyToAddress(privateKey as `0x${string}`); + console.log('āœ… Wallet configured'); + console.log(`šŸ“ Address: ${address}`); + } catch (error) { + console.error('āŒ Invalid private key in config'); + console.log('\nšŸ’” Update your wallet with:'); + console.log(' nla wallet:set --private-key '); + } +} + +/** + * Clear wallet from config + */ +export async function clearWallet(): Promise { + clearPrivateKey(); + console.log('āœ… Wallet cleared from config'); + console.log('\nšŸ’” Set a new wallet with:'); + console.log(' nla wallet:set --private-key '); +} diff --git a/cli/deployments/base-sepolia.json b/cli/deployments/base-sepolia.json new file mode 100644 index 0000000..b7346d1 --- /dev/null +++ b/cli/deployments/base-sepolia.json @@ -0,0 +1,50 @@ +{ + "network": "Base Sepolia", + "chainId": 84532, + "rpcUrl": "https://base-sepolia.infura.io/v3/e8b0c66293ae484083cf9e1d793d68bd", + "addresses": { + "eas": "", + "easSchemaRegistry": "", + "erc20EscrowObligation": "", + "erc20PaymentObligation": "", + "erc20BarterUtils": "", + "erc721EscrowObligation": "", + "erc721PaymentObligation": "", + "erc721BarterUtils": "", + "erc1155EscrowObligation": "", + "erc1155BarterUtils": "", + "erc1155PaymentObligation": "", + "tokenBundleEscrowObligation": "", + "tokenBundlePaymentObligation": "", + "tokenBundleBarterUtils": "", + "attestationEscrowObligation": "", + "attestationEscrowObligation2": "", + "attestationBarterUtils": "", + "stringObligation": "", + "trivialArbiter": "", + "trustedOracleArbiter": "", + "anyArbiter": "", + "allArbiter": "", + "intrinsicsArbiter": "", + "intrinsicsArbiter2": "", + "exclusiveRevocableConfirmationArbiter": "", + "exclusiveUnrevocableConfirmationArbiter": "", + "nonexclusiveRevocableConfirmationArbiter": "", + "nonexclusiveUnrevocableConfirmationArbiter": "", + "nativeTokenEscrowObligation": "", + "nativeTokenPaymentObligation": "", + "nativeTokenBarterUtils": "", + "recipientArbiter": "", + "attesterArbiter": "", + "schemaArbiter": "", + "uidArbiter": "", + "refUidArbiter": "", + "revocableArbiter": "", + "timeAfterArbiter": "", + "timeBeforeArbiter": "", + "timeEqualArbiter": "", + "expirationTimeAfterArbiter": "", + "expirationTimeBeforeArbiter": "", + "expirationTimeEqualArbiter": "" + } +} diff --git a/cli/index.ts b/cli/index.ts index b675be4..ea8a17c 100755 --- a/cli/index.ts +++ b/cli/index.ts @@ -12,6 +12,7 @@ import { contracts } from "alkahest-ts"; import { runDevCommand } from "./commands/dev.js"; import { runStopCommand } from "./commands/stop.js"; import { runSwitchCommand } from "./commands/switch.js"; +import { setWallet, showWallet, clearWallet } from "./commands/wallet.js"; // Get the directory name for ESM modules (compatible with both Node and Bun) const __filename = fileURLToPath(import.meta.url); @@ -29,8 +30,11 @@ Commands: deploy Deploy contracts to blockchain start-oracle Start the oracle service stop Stop all services (Anvil + Oracle) - switch [env] Switch between environments (devnet, sepolia, mainnet) + switch [env] Switch between environments (devnet, sepolia, base-sepolia, mainnet) network Show current network/environment + wallet:set Set wallet private key + wallet:show Show current wallet address + wallet:clear Clear wallet from config escrow:create Create a new escrow with natural language demand escrow:fulfill Fulfill an existing escrow escrow:collect Collect an approved escrow @@ -182,6 +186,27 @@ async function main() { return; } + // Handle wallet commands + if (command === "wallet:set") { + const privateKey = args["private-key"] as string | undefined; + if (!privateKey) { + console.error("āŒ Missing required option: --private-key"); + process.exit(1); + } + await setWallet(privateKey); + return; + } + + if (command === "wallet:show") { + await showWallet(); + return; + } + + if (command === "wallet:clear") { + await clearWallet(); + return; + } + // Handle TypeScript commands that can run directly if (command === "deploy") { await runServerCommand("deploy.js", process.argv.slice(3)); diff --git a/cli/server/deploy.ts b/cli/server/deploy.ts index 66b3b3f..4bca840 100644 --- a/cli/server/deploy.ts +++ b/cli/server/deploy.ts @@ -9,7 +9,7 @@ import { parseArgs } from "util"; import { createWalletClient, http, publicActions, parseEther } from "viem"; import { privateKeyToAccount } from "viem/accounts"; -import { mainnet, sepolia, foundry } from "viem/chains"; +import { mainnet, sepolia, baseSepolia, foundry } from "viem/chains"; import { writeFileSync, existsSync, mkdirSync } from "fs"; import { resolve } from "path"; import { fixtures, contracts } from "alkahest-ts"; @@ -23,7 +23,7 @@ Usage: bun deploy.ts [options] Options: - --network Network to deploy to: mainnet, sepolia, localhost (required) + --network Network to deploy to: mainnet, sepolia, base-sepolia, localhost (required) --rpc-url Custom RPC URL (overrides network default) --private-key Deployer's private key (required) --output Output file for deployment addresses (default: ./cli/deployments/.json) @@ -36,6 +36,7 @@ Environment Variables (alternative to CLI options): Networks: mainnet Ethereum Mainnet sepolia Ethereum Sepolia Testnet + base-sepolia Base Sepolia Testnet localhost Local development (Anvil/Hardhat) Examples: @@ -75,11 +76,13 @@ function getChain(network: string) { return mainnet; case "sepolia": return sepolia; + case "base-sepolia": + return baseSepolia; case "localhost": case "local": return foundry; default: - throw new Error(`Unknown network: ${network}. Use mainnet, sepolia, or localhost`); + throw new Error(`Unknown network: ${network}. Use mainnet, sepolia, base-sepolia, or localhost`); } } @@ -368,16 +371,6 @@ async function main() { console.log("\nšŸŽÆ Next steps:"); console.log("1. Start the oracle:"); console.log(` nla start-oracle`); - console.log("\n2. Export your private key (use a test account private key):"); - console.log(` export PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80`); - console.log("\n3. Create an escrow:"); - console.log(` nla escrow:create \\`); - console.log(` --demand "The sky is blue" \\`); - console.log(` --amount 10 \\`); - console.log(` --token ${addresses.mockERC20A} \\`); - console.log(` --oracle 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 \\`); - console.log(` --arbitration-provider "OpenAI" \\`); - console.log(` --arbitration-model "gpt-4o-mini"`); } catch (error) { console.error("āŒ Deployment failed:", error); diff --git a/cli/server/oracle.ts b/cli/server/oracle.ts index 92d2cbb..1a26355 100644 --- a/cli/server/oracle.ts +++ b/cli/server/oracle.ts @@ -2,13 +2,26 @@ import { parseArgs } from "util"; import { parseAbiParameters, createWalletClient, http, publicActions } from "viem"; import { privateKeyToAccount } from "viem/accounts"; -import { foundry } from "viem/chains"; import { makeLLMClient } from "../.."; import { existsSync, readFileSync } from "fs"; -import { resolve } from "path"; +import { resolve, dirname, join } from "path"; +import { fileURLToPath } from "url"; import { makeClient } from "alkahest-ts"; import { fixtures } from "alkahest-ts"; import { ProviderName } from "../../nla"; +import { contractAddresses } from "alkahest-ts"; +import { + getCurrentEnvironment, + getDeploymentPath, + loadEnvFile, + loadDeploymentWithDefaults, + getChainFromNetwork, + getPrivateKey +} from "../utils.js"; + +// Get the directory name for ESM modules +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); // Helper function to display usage function displayHelp() { @@ -19,38 +32,45 @@ Usage: bun oracle.ts [options] Options: - --rpc-url RPC URL for the blockchain network (required) - --private-key Private key of the oracle operator (required) - --openai-api-key OpenAI API key (optional) - --anthropic-api-key Anthropic API key (optional) - --openrouter-api-key OpenRouter API key (optional) - --perplexity-api-key Perplexity API key for search tools (optional) - --eas-contract
EAS contract address (optional) - --deployment Load addresses from deployment file (optional) + --private-key Private key of the oracle operator (optional, loaded from .env) + --openai-api-key OpenAI API key (optional, loaded from .env) + --anthropic-api-key Anthropic API key (optional, loaded from .env) + --openrouter-api-key OpenRouter API key (optional, loaded from .env) + --perplexity-api-key Perplexity API key (optional, loaded from .env) + --env Path to .env file (default: .env) + --deployment Load addresses from deployment file (optional, auto-detected from current network) --polling-interval Polling interval in milliseconds (default: 5000) --help, -h Display this help message -Environment Variables (alternative to CLI options): - RPC_URL RPC URL for the blockchain network +Environment Variables (from .env file or environment): ORACLE_PRIVATE_KEY Private key of the oracle operator OPENAI_API_KEY OpenAI API key ANTHROPIC_API_KEY Anthropic API key OPENROUTER_API_KEY OpenRouter API key PERPLEXITY_API_KEY Perplexity API key for search tools - EAS_CONTRACT_ADDRESS EAS contract address Examples: - # Using command line options - bun oracle.ts --rpc-url http://localhost:8545 --private-key 0x... --openai-api-key sk-... - - # Using deployment file - bun oracle.ts --deployment ./deployments/devnet.json --private-key 0x... --openai-api-key sk-... - - # Using environment variables - export OPENAI_API_KEY=sk-... - export RPC_URL=http://localhost:8545 - export ORACLE_PRIVATE_KEY=0x... + # Using .env file (default) bun oracle.ts + + # Using custom .env file + bun oracle.ts --env /path/to/.env.production + + # Override with command-line parameters + bun oracle.ts --private-key 0x... --openai-api-key sk-... + + # Using specific deployment file + bun oracle.ts --deployment ./deployments/sepolia.json + + # Mix of .env and command-line parameters + bun oracle.ts --openai-api-key sk-... --env .env.local + +Example .env file: + ORACLE_PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 + OPENAI_API_KEY=sk-... + ANTHROPIC_API_KEY=sk-ant-... + OPENROUTER_API_KEY=sk-or-... + PERPLEXITY_API_KEY=pplx-... `); } @@ -59,13 +79,12 @@ function parseCliArgs() { const { values } = parseArgs({ args: process.argv.slice(2), options: { - "rpc-url": { type: "string" }, "private-key": { type: "string" }, "openai-api-key": { type: "string" }, "anthropic-api-key": { type: "string" }, "openrouter-api-key": { type: "string" }, "perplexity-api-key": { type: "string" }, - "eas-contract": { type: "string" }, + "env": { type: "string" }, "deployment": { type: "string" }, "polling-interval": { type: "string" }, "help": { type: "boolean", short: "h" }, @@ -76,16 +95,6 @@ function parseCliArgs() { return values; } -// Load deployment file -function loadDeployment(filePath: string) { - if (!existsSync(filePath)) { - throw new Error(`Deployment file not found: ${filePath}`); - } - - const content = readFileSync(filePath, "utf-8"); - return JSON.parse(content); -} - // Main function async function main() { try { @@ -97,27 +106,33 @@ async function main() { process.exit(0); } - let rpcUrl = args["rpc-url"] || process.env.RPC_URL; - let easContract = args["eas-contract"] || process.env.EAS_CONTRACT_ADDRESS; - let deploymentAddresses = null; - - // Load deployment file if provided - if (args.deployment) { - console.log(`šŸ“„ Loading deployment from: ${args.deployment}\n`); - const deployment = loadDeployment(args.deployment); - deploymentAddresses = deployment.addresses; - - if (!rpcUrl) { - rpcUrl = deployment.rpcUrl; - } - if (!easContract) { - easContract = deployment.addresses.eas; - } - - console.log(`āœ… Loaded deployment (${deployment.network})\n`); + // Load .env file + const envPath = args.env || ".env"; + const resolvedEnvPath = resolve(process.cwd(), envPath); + + if (existsSync(resolvedEnvPath)) { + console.log(`šŸ“ Loading environment from: ${resolvedEnvPath}\n`); + loadEnvFile(resolvedEnvPath); + } else if (args.env) { + // Only error if user explicitly specified an env file + console.error(`āŒ Error: .env file not found: ${resolvedEnvPath}`); + process.exit(1); } - const privateKey = args["private-key"] || process.env.ORACLE_PRIVATE_KEY; + // Load deployment file (auto-detects current network if not specified) + const currentEnv = getCurrentEnvironment(); + const deploymentFile = args.deployment; + + if (deploymentFile) { + console.log(`ļæ½ Loading deployment from: ${deploymentFile}\n`); + } else { + console.log(`ļæ½ Auto-detected environment: ${currentEnv}\n`); + } + + const deployment = loadDeploymentWithDefaults(deploymentFile); + console.log(`āœ… Loaded deployment (${deployment.network})\n`); + + const privateKey = args["private-key"] || getPrivateKey(); const openaiApiKey = args["openai-api-key"] || process.env.OPENAI_API_KEY; const anthropicApiKey = args["anthropic-api-key"] || process.env.ANTHROPIC_API_KEY; const openrouterApiKey = args["openrouter-api-key"] || process.env.OPENROUTER_API_KEY; @@ -125,29 +140,39 @@ async function main() { const pollingInterval = parseInt(args["polling-interval"] || "5000"); // Validate required parameters - if (!rpcUrl) { - console.error("āŒ Error: RPC URL is required. Use --rpc-url or set RPC_URL environment variable."); + if (!deployment?.rpcUrl) { + console.error("āŒ Error: RPC URL not found in deployment file."); + console.error(" Current environment:", getCurrentEnvironment()); + console.error(" Make sure the deployment file exists for your current network."); console.error("Run with --help for usage information."); process.exit(1); } if (!privateKey) { - console.error("āŒ Error: Private key is required. Use --private-key or set ORACLE_PRIVATE_KEY environment variable."); - console.error("Run with --help for usage information."); + console.error("āŒ Error: Private key is required."); + console.error("\nšŸ’” You can either:"); + console.error(" 1. Set it globally: nla wallet:set --private-key "); + console.error(" 2. Use for this command only: --private-key "); + console.error(" 3. Set ORACLE_PRIVATE_KEY in .env file"); + console.error(" 4. Set PRIVATE_KEY environment variable"); + console.error("\nRun with --help for usage information."); process.exit(1); } // Check if at least one API key is provided if (!openaiApiKey && !anthropicApiKey && !openrouterApiKey) { console.error("āŒ Error: At least one LLM provider API key is required."); - console.error(" Set one of: OPENAI_API_KEY, ANTHROPIC_API_KEY, or OPENROUTER_API_KEY"); + console.error(" Set one of these in your .env file:"); + console.error(" - OPENAI_API_KEY"); + console.error(" - ANTHROPIC_API_KEY"); + console.error(" - OPENROUTER_API_KEY"); console.error("Run with --help for usage information."); process.exit(1); } console.log("šŸš€ Starting Natural Language Agreement Oracle...\n"); console.log("Configuration:"); - console.log(` šŸ“” RPC URL: ${rpcUrl}`); + console.log(` šŸ“” RPC URL: ${deployment.rpcUrl}`); console.log(` šŸ”‘ Oracle Key: ${privateKey.slice(0, 6)}...${privateKey.slice(-4)}`); // Show available providers @@ -161,21 +186,24 @@ async function main() { console.log(` šŸ” Perplexity Search: Enabled`); } - if (easContract) { - console.log(` šŸ“ EAS Contract: ${easContract}`); + if (deployment.addresses.eas) { + console.log(` šŸ“ EAS Contract: ${deployment.addresses.eas}`); } console.log(` ā±ļø Polling Interval: ${pollingInterval}ms\n`); // Create wallet client + const chain = getChainFromNetwork(deployment.network); const account = privateKeyToAccount(privateKey as `0x${string}`); const walletClient = createWalletClient({ account, - chain: foundry, - transport: http(rpcUrl), + chain, + transport: http(deployment.rpcUrl), }).extend(publicActions) as any; + + // Create alkahest client - const client = makeClient(walletClient, deploymentAddresses || { eas: easContract }); + const client = makeClient(walletClient, deployment.addresses); console.log(`āœ… Oracle initialized with address: ${account.address}\n`); @@ -234,12 +262,25 @@ async function main() { console.log(` Obligation: "${obligationItem}"`); const trustedOracleDemandData = client.arbiters.general.trustedOracle.decodeDemand(demand); + console.log(` DEBUG - trustedOracleDemandData:`, trustedOracleDemandData); + const nlaDemandData = llmClient.llm.decodeDemand(trustedOracleDemandData.data); + console.log(` DEBUG - nlaDemandData:`, nlaDemandData); + console.log(` Demand: "${nlaDemandData.demand}"`); + console.log(` Provider: ${nlaDemandData.arbitrationProvider}`); console.log(` Model: ${nlaDemandData.arbitrationModel}`); + // Validate the demand data before proceeding + if (!nlaDemandData.demand || !nlaDemandData.arbitrationModel || nlaDemandData.arbitrationModel.includes('\u0000')) { + console.error(` āŒ Invalid demand data - contains null bytes or empty fields`); + console.error(` This usually means the demand was encoded incorrectly`); + console.error(` Skipping this attestation (throwing error to avoid on-chain recording)...\n`); + throw new Error('Invalid demand data - skipping attestation'); + } + // Perform arbitration using LLM - console.log(` šŸ¤” Arbitrating with AI...`); + console.log(` šŸ¤” Arbitrating with ${nlaDemandData.arbitrationProvider}...`); const result = await llmClient.llm.arbitrate( nlaDemandData, obligationItem @@ -249,14 +290,21 @@ async function main() { return result; } catch (error) { console.error(` āŒ Error during arbitration:`, error); - throw error; + console.error(` Continuing to listen for new requests...\n`); + return false; // Return false instead of throwing to keep oracle running } }, { onAfterArbitrate: async (decision: any) => { - console.log(` šŸ“ Arbitration decision recorded on-chain`); - console.log(` Decision UID: ${decision.attestation.uid}`); - console.log(` Result: ${decision.decision ? "āœ… Fulfilled" : "āŒ Not Fulfilled"}\n`); + try { + console.log(` šŸ“ Arbitration decision recorded on-chain`); + console.log(` Decision UID: ${decision.attestation.uid}`); + console.log(` Result: ${decision.decision ? "āœ… Fulfilled" : "āŒ Not Fulfilled"}\n`); + } catch (error: any) { + console.error(` āš ļø Failed to record arbitration on-chain:`, error.message); + console.error(` This may be due to transaction conflicts or gas issues`); + console.error(` Continuing to listen for new requests...\n`); + } }, pollingInterval, }, @@ -264,6 +312,36 @@ async function main() { console.log("✨ Oracle is now running. Press Ctrl+C to stop.\n"); + // Show next steps for creating escrow + if (deployment) { + console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); + console.log("šŸ“ Next Steps - Create Your First Escrow:\n"); + + if (currentEnv === 'devnet') { + console.log("1. Export your private key (use a test account):"); + console.log(" export PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80\n"); + } else { + console.log("1. Export your private key:"); + console.log(" export PRIVATE_KEY=\n"); + } + + console.log("2. Create an escrow:"); + console.log(" nla escrow:create \\"); + console.log(" --demand \"The sky is blue\" \\"); + console.log(" --amount 10 \\"); + + if (currentEnv === 'devnet' && deployment.addresses.mockERC20A) { + console.log(` --token ${deployment.addresses.mockERC20A} \\`); + } else { + console.log(" --token \\"); + } + + console.log(` --oracle ${account.address} \\`); + console.log(" --arbitration-provider \"OpenAI\" \\"); + console.log(" --arbitration-model \"gpt-4o-mini\""); + console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"); + } + // Handle graceful shutdown const shutdown = async () => { console.log("\n\nšŸ›‘ Shutting down oracle..."); diff --git a/cli/utils.ts b/cli/utils.ts index 0a96f36..ed67f99 100644 --- a/cli/utils.ts +++ b/cli/utils.ts @@ -2,22 +2,247 @@ * Shared utilities for NLA CLI */ -import { foundry, sepolia, mainnet } from "viem/chains"; +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 { - switch (network.toLowerCase()) { + // 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; +} { + 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 = {}; + + // 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, + }; +} diff --git a/package.json b/package.json index bc44fce..585849e 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "@perplexity-ai/ai-sdk": "^0.1.2", "@viem/anvil": "^0.0.10", "ai": "^6.0.5", - "alkahest-ts": "github:arkhai-io/alkahest", + "alkahest-ts": "^0.6.1", "arktype": "^2.1.23", "viem": "^2.42.1", "zod": "^3.25.76"