Merge pull request #5 from arkhai-io/refactor

Refactor
This commit is contained in:
thanhngoc541 2026-02-01 15:18:49 +07:00 committed by GitHub
commit 079cf4dec9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 619 additions and 297 deletions

View File

@ -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=="],
}
}

View File

@ -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 <your-key>");
console.error(" 2. Use for this command only: --private-key <your-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.");

View File

@ -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 <your-key>");
console.error(" 2. Use for this command only: --private-key <your-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(

View File

@ -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 <your-key>");
console.error(" 2. Use for this command only: --private-key <your-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.");

View File

@ -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('');

View File

@ -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 <environment>`);
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`);

72
cli/commands/wallet.ts Normal file
View File

@ -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<void> {
// 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<void> {
const privateKey = getPrivateKey();
if (!privateKey) {
console.log(' No wallet configured');
console.log('\n💡 Set your wallet with:');
console.log(' nla wallet:set --private-key <your-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 <your-key>');
}
}
/**
* Clear wallet from config
*/
export async function clearWallet(): Promise<void> {
clearPrivateKey();
console.log('✅ Wallet cleared from config');
console.log('\n💡 Set a new wallet with:');
console.log(' nla wallet:set --private-key <your-key>');
}

View File

@ -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": ""
}
}

View File

@ -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));

View File

@ -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 <name> Network to deploy to: mainnet, sepolia, localhost (required)
--network <name> Network to deploy to: mainnet, sepolia, base-sepolia, localhost (required)
--rpc-url <url> Custom RPC URL (overrides network default)
--private-key <key> Deployer's private key (required)
--output <path> Output file for deployment addresses (default: ./cli/deployments/<network>.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);

View File

@ -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 <url> RPC URL for the blockchain network (required)
--private-key <key> Private key of the oracle operator (required)
--openai-api-key <key> OpenAI API key (optional)
--anthropic-api-key <key> Anthropic API key (optional)
--openrouter-api-key <key> OpenRouter API key (optional)
--perplexity-api-key <key> Perplexity API key for search tools (optional)
--eas-contract <address> EAS contract address (optional)
--deployment <file> Load addresses from deployment file (optional)
--private-key <key> Private key of the oracle operator (optional, loaded from .env)
--openai-api-key <key> OpenAI API key (optional, loaded from .env)
--anthropic-api-key <key> Anthropic API key (optional, loaded from .env)
--openrouter-api-key <key> OpenRouter API key (optional, loaded from .env)
--perplexity-api-key <key> Perplexity API key (optional, loaded from .env)
--env <file> Path to .env file (default: .env)
--deployment <file> Load addresses from deployment file (optional, auto-detected from current network)
--polling-interval <ms> 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(`<EFBFBD> Loading deployment from: ${deploymentFile}\n`);
} else {
console.log(`<EFBFBD> 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 <your-key>");
console.error(" 2. Use for this command only: --private-key <your-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=<your-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 <ERC20_TOKEN_ADDRESS> \\");
}
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...");

View File

@ -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<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,
};
}

View File

@ -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"