#!/usr/bin/env node import { parseArgs } from "util"; import { createWalletClient, http, publicActions, fromHex } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { makeLLMClient } from "../.."; import { existsSync, readFileSync } from "fs"; 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() { console.log(` Natural Language Agreement Oracle CLI Usage: bun oracle.ts [options] Options: --private-key Private key of the oracle operator (optional, loaded from .env) --rpc-url RPC URL to connect to (optional, overrides deployment file) --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 (from .env file or environment): 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 Examples: # 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 # Using custom RPC URL bun oracle.ts --rpc-url https://eth-mainnet.g.alchemy.com/v2/YOUR-KEY # Mix of .env and command-line parameters bun oracle.ts --openai-api-key sk-... --env .env.local Example .env file: PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 OPENAI_API_KEY=sk-... ANTHROPIC_API_KEY=sk-ant-... OPENROUTER_API_KEY=sk-or-... PERPLEXITY_API_KEY=pplx-... `); } // Parse command line arguments function parseCliArgs() { const { values } = parseArgs({ args: process.argv.slice(2), options: { "private-key": { type: "string" }, "rpc-url": { type: "string" }, "openai-api-key": { type: "string" }, "anthropic-api-key": { type: "string" }, "openrouter-api-key": { type: "string" }, "perplexity-api-key": { type: "string" }, "env": { type: "string" }, "deployment": { type: "string" }, "polling-interval": { type: "string" }, "help": { type: "boolean", short: "h" }, }, strict: true, }); return values; } // Main function async function main() { try { const args = parseCliArgs(); // Display help if requested if (args.help) { displayHelp(); process.exit(0); } // 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); } // 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 rpcUrl = args["rpc-url"] || deployment.rpcUrl; 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; const perplexityApiKey = args["perplexity-api-key"] || process.env.PERPLEXITY_API_KEY; const pollingInterval = parseInt(args["polling-interval"] || "5000"); // Validate required parameters if (!rpcUrl) { console.error("āŒ Error: RPC URL not found."); console.error(" Please either:"); console.error(" 1. Use --rpc-url "); console.error(" 2. Use a deployment file with rpcUrl set"); console.error(" 3. Set RPC_URL environment variable"); console.error("Run with --help for usage information."); process.exit(1); } if (!privateKey) { 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); } // 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 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(` šŸ”‘ Oracle Key: ${privateKey.slice(0, 6)}...${privateKey.slice(-4)}`); // Show available providers const availableProviders = []; if (openaiApiKey) availableProviders.push("OpenAI"); if (anthropicApiKey) availableProviders.push("Anthropic"); if (openrouterApiKey) availableProviders.push("OpenRouter"); console.log(` šŸ¤– AI Providers: ${availableProviders.join(", ")}`); if (perplexityApiKey) { console.log(` šŸ” Perplexity Search: Enabled`); } 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, transport: http(rpcUrl), }).extend(publicActions) as any; // Create alkahest client const client = makeClient(walletClient, deployment.addresses); console.log(`āœ… Oracle initialized with address: ${account.address}\n`); // Create LLM client const llmClient = client.extend(() => ({ llm: makeLLMClient([]), })); // Add all available providers if (openaiApiKey) { llmClient.llm.addProvider({ providerName: ProviderName.OpenAI, apiKey: openaiApiKey, perplexityApiKey: perplexityApiKey, }); console.log("āœ… OpenAI provider configured"); } if (anthropicApiKey) { llmClient.llm.addProvider({ providerName: ProviderName.Anthropic, apiKey: anthropicApiKey, perplexityApiKey: perplexityApiKey, }); console.log("āœ… Anthropic provider configured"); } if (openrouterApiKey) { llmClient.llm.addProvider({ providerName: ProviderName.OpenRouter, apiKey: openrouterApiKey, perplexityApiKey: perplexityApiKey, }); console.log("āœ… OpenRouter provider configured"); } console.log("\nšŸŽÆ LLM Arbitrator configured and ready\n"); console.log("šŸ‘‚ Listening for arbitration requests...\n"); // Get current block number to avoid scanning from genesis (public RPCs limit getLogs range) const currentBlock = await walletClient.getBlockNumber(); console.log(` šŸ“¦ Starting from block: ${currentBlock}\n`); // Start listening and arbitrating const { unwatch } = await client.arbiters.general.trustedOracle.arbitrateMany( async ({ attestation, demand }) => { console.log(`\nšŸ“Ø New arbitration request received!`); console.log(` Attestation UID: ${attestation.uid}`); try { // Extract obligation data from CommitRevealObligation const commitRevealData = client.commitReveal.decode(attestation.data); const obligationItem = fromHex(commitRevealData.payload, 'string'); 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 ${nlaDemandData.arbitrationProvider}...`); const result = await llmClient.llm.arbitrate( nlaDemandData, obligationItem ); console.log(` ✨ Arbitration result: ${result ? "āœ… APPROVED" : "āŒ REJECTED"}`); return result; } catch (error) { console.error(` āŒ Error during arbitration:`, 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) => { 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, fromBlock: currentBlock, }, ); 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 === 'anvil') { 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 === 'anvil' && 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..."); unwatch(); console.log("šŸ‘‹ Oracle stopped gracefully"); process.exit(0); }; process.on("SIGINT", shutdown); process.on("SIGTERM", shutdown); // Keep the process alive await new Promise(() => { }); } catch (error) { console.error("āŒ Fatal error:", error); process.exit(1); } } // Run the CLI main();