#!/usr/bin/env node import { parseArgs } from "util"; import { spawnSync } from "child_process"; import { existsSync, readFileSync } from "fs"; import { fileURLToPath } from "url"; import { dirname, join } from "path"; import { createPublicClient, http, parseAbiParameters, decodeAbiParameters } from "viem"; import { foundry } from "viem/chains"; import { contracts } from "alkahest-ts"; import { runDevCommand } from "./commands/dev.js"; import { runStopCommand } from "./commands/stop.js"; import { runSwitchCommand } from "./commands/switch.js"; // Get the directory name for ESM modules (compatible with both Node and Bun) const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); // Helper function to display usage function displayHelp() { console.log(` Natural Language Agreement CLI Usage: nla [options] Commands: dev Start local development environment (Anvil + Deploy + Oracle) 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) network Show current network/environment escrow:create Create a new escrow with natural language demand escrow:fulfill Fulfill an existing escrow escrow:collect Collect an approved escrow escrow:status Check the status of an escrow help Display this help message Options (vary by command): --demand Natural language demand (create) --amount Amount of tokens to escrow (create) --token
ERC20 token contract address (create) --oracle
Oracle address (create, fulfill) --escrow-uid Escrow UID (fulfill, collect, status) --fulfillment Fulfillment text (fulfill) --fulfillment-uid Fulfillment UID (collect) --private-key Private key (all commands) --rpc-url RPC URL (default: http://localhost:8545) --deployment Load addresses from deployment file --arbitration-provider Arbitration provider (create, default: OpenAI) --arbitration-model Arbitration model (create, default: gpt-4o-mini) --arbitration-prompt Custom arbitration prompt (create, optional) --env Path to .env file (dev, default: .env) Environment Variables: PRIVATE_KEY Private key for transactions RPC_URL RPC URL for blockchain network OPENAI_API_KEY OpenAI API key (for create command) Examples: # Start development environment nla dev # Start development with custom .env file nla dev --env /path/to/.env.production # Deploy contracts nla deploy # Start oracle nla start-oracle # Stop all services nla stop # Create an escrow nla escrow:create \\ --demand "The sky is blue" \\ --amount 10 \\ --token 0xa513E6E4b8f2a923D98304ec87F64353C4D5C853 \\ --oracle 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 # Fulfill an escrow nla escrow:fulfill \\ --escrow-uid 0x... \\ --fulfillment "The sky appears blue today" \\ --oracle 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 # Collect an escrow nla escrow:collect \\ --escrow-uid 0x... \\ --fulfillment-uid 0x... # Check escrow status nla escrow:status --escrow-uid 0x... `); } // Parse command line arguments function parseCliArgs() { const args = process.argv.slice(2); if (args.length === 0) { displayHelp(); process.exit(0); } const command = args[0]; if (command === "help" || command === "--help" || command === "-h") { displayHelp(); process.exit(0); } const { values } = parseArgs({ args: args.slice(1), options: { "demand": { type: "string" }, "amount": { type: "string" }, "token": { type: "string" }, "oracle": { type: "string" }, "escrow-uid": { type: "string" }, "fulfillment": { type: "string" }, "fulfillment-uid": { type: "string" }, "private-key": { type: "string" }, "rpc-url": { type: "string" }, "deployment": { type: "string" }, "arbitration-provider": { type: "string" }, "arbitration-model": { type: "string" }, "arbitration-prompt": { type: "string" }, "env": { type: "string" }, "environment": { type: "string" }, }, strict: command !== "switch" && command !== "network", // Allow positional args for switch command allowPositionals: command === "switch" || command === "network", }); return { command, ...values }; } // Server command handler (for deploy.ts, oracle.ts) async function runServerCommand(scriptName: string, args: string[] = []) { const scriptPath = join(__dirname, "server", scriptName); // Run the TypeScript file directly const result = spawnSync("bun", ["run", scriptPath, ...args], { stdio: "inherit", cwd: process.cwd(), }); process.exit(result.status || 0); } // Main function async function main() { try { const args = parseCliArgs(); const command = args.command; // Handle dev and stop commands if (command === "dev") { await runDevCommand(__dirname, args.env as string | undefined); return; } if (command === "stop") { await runStopCommand(); return; } if (command === "switch") { // Get environment from either --environment flag or second positional arg const env = args.environment as string | undefined || process.argv[3]; runSwitchCommand(env); return; } if (command === "network") { // Show current network (same as switch with no args) runSwitchCommand(); return; } // Handle TypeScript commands that can run directly if (command === "deploy") { await runServerCommand("deploy.js", process.argv.slice(3)); return; } if (command === "start-oracle") { await runServerCommand("oracle.js", process.argv.slice(3)); return; } // Get the script path based on command let scriptPath: string; switch (command) { case "escrow:create": scriptPath = "./client/create-escrow.js"; break; case "escrow:fulfill": scriptPath = "./client/fulfill-escrow.js"; break; case "escrow:collect": scriptPath = "./client/collect-escrow.js"; break; case "escrow:status": await runStatusCommand(args); return; default: console.error(`āŒ Unknown command: ${command}`); console.error("Run 'nla help' for usage information."); process.exit(1); } // Run the command as a subprocess with the args (excluding the command name) const { spawnSync } = await import("child_process"); const fullScriptPath = join(__dirname, scriptPath); // Build args array without the command name const commandArgs = process.argv.slice(3); // Skip node, script, and command const result = spawnSync("bun", ["run", fullScriptPath, ...commandArgs], { stdio: "inherit", cwd: process.cwd(), }); process.exit(result.status || 0); } catch (error) { console.error("āŒ Error:", error); process.exit(1); } } // Status command handler async function runStatusCommand(args: any) { const escrowUid = args["escrow-uid"]; const rpcUrl = args["rpc-url"] || process.env.RPC_URL || "http://localhost:8545"; const deploymentFile = args["deployment"]; if (!escrowUid) { console.error("āŒ Error: --escrow-uid is required for status command"); process.exit(1); } console.log("šŸ” Checking Escrow Status\n"); console.log(`Configuration:`); console.log(` šŸ“¦ Escrow UID: ${escrowUid}`); console.log(` 🌐 RPC URL: ${rpcUrl}\n`); // Import required modules const { createPublicClient, http, parseAbiParameters } = await import("viem"); const { foundry } = await import("viem/chains"); const { existsSync, readFileSync } = await import("fs"); // Load deployment addresses let addresses: any = {}; if (deploymentFile && existsSync(deploymentFile)) { const deployment = JSON.parse(readFileSync(deploymentFile, "utf-8")); addresses = deployment.addresses; } // Create public client const publicClient = createPublicClient({ chain: foundry, transport: http(rpcUrl), }); if (!addresses.eas) { console.error("āŒ Error: EAS address not found. Use --deployment to specify deployment file."); process.exit(1); } // Get escrow attestation console.log("šŸ“‹ Fetching escrow details...\n"); const escrow = await publicClient.readContract({ address: addresses.eas, abi: contracts.IEAS.abi.abi, functionName: "getAttestation", args: [escrowUid], }) as any; console.log("šŸ“¦ Escrow Information:"); console.log(` UID: ${escrow.uid}`); console.log(` Schema: ${escrow.schema}`); console.log(` Attester: ${escrow.attester}`); console.log(` Recipient: ${escrow.recipient}`); console.log(` Revoked: ${escrow.revocationTime > 0n ? "Yes āŒ" : "No āœ…"}`); // Try to decode the data try { const llmAbi = parseAbiParameters("(string demand, string arbitrationModel, address arbitrator)"); const decoded = await import("viem").then(m => m.decodeAbiParameters(llmAbi, escrow.data) ); console.log(`\nšŸ“ Escrow Details:`); console.log(` Demand: "${decoded[0].demand}"`); console.log(` Model: ${decoded[0].arbitrationModel}`); console.log(` Arbitrator: ${decoded[0].arbitrator}`); } catch (e) { console.log(`\nšŸ“ Raw Data: ${escrow.data}`); } // Check for fulfillments console.log(`\nšŸ”Ž Checking for fulfillments...`); const filter = await publicClient.createContractEventFilter({ address: addresses.eas, abi: contracts.IEAS.abi.abi, eventName: "Attested", fromBlock: 0n, }); const events = await publicClient.getFilterLogs({ filter }); // Find fulfillments that reference this escrow const fulfillments = events.filter((event: any) => { return (event as any).args?.refUID === escrowUid; }); if (fulfillments.length === 0) { console.log(` No fulfillments found yet`); } else { console.log(` Found ${fulfillments.length} fulfillment(s):\n`); for (const fulfillment of fulfillments) { const fulfillmentUid = (fulfillment as any).args?.uid; const fulfillmentAttestation = await publicClient.readContract({ address: addresses.eas, abi: contracts.IEAS.abi.abi, functionName: "getAttestation", args: [fulfillmentUid], }) as any; console.log(` šŸ“Ø Fulfillment UID: ${fulfillmentUid}`); console.log(` Attester: ${fulfillmentAttestation.attester}`); console.log(` Revoked: ${fulfillmentAttestation.revocationTime > 0n ? "Yes āŒ" : "No āœ…"}`); // Check for arbitration decision const decisions = events.filter((e: any) => (e as any).args?.refUID === fulfillmentUid); if (decisions.length > 0) { console.log(` āš–ļø Arbitration: Decision recorded`); for (const decision of decisions) { const decisionUid = (decision as any).args?.uid; const decisionAttestation = await publicClient.readContract({ address: addresses.eas, abi: contracts.IEAS.abi.abi, functionName: "getAttestation", args: [decisionUid], }) as any; try { const decisionAbi = parseAbiParameters("(bool item)"); const decisionData = await import("viem").then(m => m.decodeAbiParameters(decisionAbi, decisionAttestation.data) ); console.log(` Result: ${decisionData[0].item ? "āœ… APPROVED" : "āŒ REJECTED"}`); } catch (e) { console.log(` Result: Unknown`); } } } else { console.log(` āš–ļø Arbitration: Pending...`); } console.log(); } } console.log("✨ Status check complete!\n"); } // Run the CLI main();