natural-language-agreements/cli/server/oracle.ts

368 lines
15 KiB
JavaScript
Raw Blame History

#!/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 <key> Private key of the oracle operator (optional, loaded from .env)
--rpc-url <url> RPC URL to connect to (optional, overrides deployment file)
--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 (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(`<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 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 <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 <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);
}
// 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");
// 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,
},
);
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=<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 === 'anvil' && 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...");
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();