diff --git a/.env.example b/.env.example index 41595b2..2dd1a25 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,40 @@ -OPENAI_API_KEY=sk-proj... \ No newline at end of file +# Natural Language Agreement Oracle Configuration + +# ================================ +# DEPLOYMENT CONFIGURATION +# ================================ + +# Deployer's private key (used for contract deployment) +# IMPORTANT: This should be a funded account with enough ETH for gas +# NEVER commit your real private key! +# The key below is from Anvil's default accounts - DO NOT USE IN PRODUCTION +DEPLOYER_PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 + +# ================================ +# ORACLE CONFIGURATION +# ================================ + +# Blockchain RPC URL (required for oracle) +# Examples: +# Local: http://localhost:8545 +# Sepolia: https://sepolia.infura.io/v3/YOUR-INFURA-KEY +# Mainnet: https://mainnet.infura.io/v3/YOUR-INFURA-KEY +RPC_URL=http://localhost:8545 + +# Oracle operator's private key (required for oracle) +# This account submits arbitration decisions on-chain +# IMPORTANT: Ensure this account has sufficient ETH for gas! +# The key below is from Anvil's default accounts - DO NOT USE IN PRODUCTION +ORACLE_PRIVATE_KEY=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d + +# EAS (Ethereum Attestation Service) Contract Address (optional) +# If you used the deploy script, get this from deployments/.json +# EAS_CONTRACT_ADDRESS=0x... + +# ================================ +# AI CONFIGURATION +# ================================ + +# OpenAI API Key (required for oracle) +# Get your API key from https://platform.openai.com/api-keys +OPENAI_API_KEY=sk-proj... diff --git a/.gitignore b/.gitignore index 5ce37b1..8d6c402 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,11 @@ coverage logs _.log report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json +anvil.log + +# process ids +*.pid +.anvil.pid # dotenv environment variable files .env diff --git a/QUICKSTART.md b/QUICKSTART.md new file mode 100644 index 0000000..7bc28f8 --- /dev/null +++ b/QUICKSTART.md @@ -0,0 +1,75 @@ +# Quick Start Guide + +Get your Natural Language Agreement Oracle running in **under 2 minutes**! + +## Prerequisites + +- [Bun](https://bun.sh) installed +- [Foundry](https://book.getfoundry.sh/getting-started/installation) installed +- Both `alkahest` and `natural-language-agreements` repos cloned in same parent directory +- OpenAI API key + +## One-Command Setup + +```bash +export OPENAI_API_KEY=sk-your-key-here +./scripts/dev.sh +``` + +Done! The oracle is now running and listening for arbitration requests. + +**To stop:** +```bash +./scripts/stop.sh +``` + +## What Just Happened? + +The script: +1. ✅ Checked prerequisites +2. ✅ Started Anvil (local blockchain) +3. ✅ Deployed all contracts +4. ✅ Started the oracle + +## Test It + +In another terminal: +```bash +bun test tests/nlaOracle.test.ts +``` + +Watch the oracle terminal to see it process the arbitration! + +## Manual Steps (Optional) + +If you want more control: + +```bash +# Terminal 1: Blockchain +anvil + +# Terminal 2: Deploy and start oracle +bun run deploy +bun run oracle + +# Terminal 3: Test +bun test tests/nlaOracle.test.ts +``` + +## Deploy to Testnet + +```bash +# Get Sepolia ETH from faucet first +export DEPLOYER_PRIVATE_KEY=0x... +bun run setups/deploy.ts --network sepolia --rpc-url https://sepolia.infura.io/v3/YOUR-KEY + +# Start oracle +export ORACLE_PRIVATE_KEY=0x... +export OPENAI_API_KEY=sk-... +bun run setups/oracle.ts sepolia +``` + +## Need Help? + +- Full docs: [README.md](README.md) +- Example test: `tests/nlaOracle.test.ts` diff --git a/README.md b/README.md index af69638..e7fe346 100644 --- a/README.md +++ b/README.md @@ -41,8 +41,188 @@ cd natural-language-agreements bun install ``` +## Quick Start + +### Option 1: Automated Setup (Easiest - 1 command!) + +Set your OpenAI API key and run everything: + +```bash +export OPENAI_API_KEY=sk-your-key-here +./scripts/dev.sh +``` + +This will: +- ✅ Check all prerequisites +- ✅ Start Anvil (local blockchain) +- ✅ Deploy all contracts +- ✅ Start the oracle +- ✅ Ready to test! + +To stop everything: +```bash +./scripts/stop.sh +``` + +### Option 2: Manual Setup (Step by Step) + +#### 1. Start Local Blockchain + +```bash +# Terminal 1: Start Anvil +anvil +``` + +#### 2. Deploy Contracts + +```bash +# Terminal 2: Deploy to localhost +export OPENAI_API_KEY=sk-your-key-here +./scripts/deploy.sh localhost +``` + +This creates `deployments/localhost.json` with all contract addresses. + +#### 3. Start Oracle + +```bash +# Terminal 2 (or 3): Start oracle +./scripts/start-oracle.sh localhost +``` + +#### 4. Test It + +```bash +# Terminal 3 (or 4): Run tests +bun test tests/nlaOracle.test.ts +``` + +Watch the oracle terminal - you'll see it process arbitration requests in real-time! + +## Deployment to Other Networks + +### Sepolia Testnet + +```bash +# 1. Get Sepolia ETH from faucet +# 2. Set your keys +export DEPLOYER_PRIVATE_KEY=0x... +export ORACLE_PRIVATE_KEY=0x... +export OPENAI_API_KEY=sk-... + +# 3. Deploy +./scripts/deploy.sh sepolia https://sepolia.infura.io/v3/YOUR-KEY + +# 4. Start oracle +./scripts/start-oracle.sh sepolia +``` + +### Mainnet + +```bash +# ⚠️ PRODUCTION - Be careful! +export DEPLOYER_PRIVATE_KEY=0x... +export ORACLE_PRIVATE_KEY=0x... +export OPENAI_API_KEY=sk-... + +# Deploy +./scripts/deploy.sh mainnet https://mainnet.infura.io/v3/YOUR-KEY + +# Start oracle (consider running as a service) +./scripts/start-oracle.sh mainnet +``` + +## Available Scripts + +```bash +./scripts/dev.sh # Complete local setup (all-in-one) +./scripts/deploy.sh [network] # Deploy contracts to network +./scripts/start-oracle.sh [network] # Start oracle for network +./scripts/stop.sh # Stop all services +``` + +## Production Deployment + +For production, run the oracle as a background service: + +### Using systemd (Linux) + +```bash +# Copy service file +sudo cp deployment/nla-oracle.service /etc/systemd/system/ + +# Edit the service file with your paths and config +sudo nano /etc/systemd/system/nla-oracle.service + +# Enable and start +sudo systemctl enable nla-oracle +sudo systemctl start nla-oracle + +# View logs +sudo journalctl -u nla-oracle -f +``` + +### Using nohup (Simple) + +```bash +# Start in background +nohup ./scripts/start-oracle.sh mainnet > oracle.log 2>&1 & + +# Save PID +echo $! > oracle.pid + +# Stop later +kill $(cat oracle.pid) +``` + +### Using screen (Simple) + +```bash +# Start screen session +screen -S oracle + +# Run oracle +./scripts/start-oracle.sh mainnet + +# Detach: Ctrl+A, then D +# Reattach: screen -r oracle +``` + +## Monitoring + +### View Oracle Logs + +```bash +# If using systemd +sudo journalctl -u nla-oracle -f + +# If using nohup +tail -f oracle.log + +# If using Anvil +tail -f anvil.log +``` + +### Check Oracle Status + +```bash +# Check if oracle is running +ps aux | grep "bun run oracle" + +# Check if Anvil is running +lsof -i :8545 +``` + ## Usage +### Running Tests + +```bash +bun test +``` + +### Development Mode + To run: ```bash @@ -52,3 +232,55 @@ bun run index.ts ## Development This project was created using `bun init` in bun v1.2.20. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime. + +## Project Structure + +``` +natural-language-agreements/ +├── oracle.ts # Oracle CLI application +├── deploy.ts # Contract deployment script +├── index.ts # Development entry point +├── clients/ +│ └── nla.ts # Natural Language Agreement client +├── tests/ +│ ├── nla.test.ts # Basic tests +│ └── nlaOracle.test.ts # Oracle arbitration tests +├── deployments/ # Deployment addresses (generated) +│ ├── localhost.json +│ ├── sepolia.json +│ └── mainnet.json +├── package.json +└── README.md +``` + +## Troubleshooting + +### "Cannot find module 'alkahest-ts'" +- Ensure alkahest is cloned in the parent directory +- Run `bun install` in both alkahest and this project + +### "Deployer has no ETH" +- Fund your deployer account before running deployment +- For testnets, use a faucet + +### "Oracle not detecting arbitration requests" +- Verify RPC URL is correct and accessible +- Check that EAS contract address matches deployment +- Ensure oracle has ETH for gas +- Check polling interval (try lowering it) + +### "OpenAI API errors" +- Verify API key is valid and active +- Check OpenAI usage limits and billing +- Ensure model name is correct (e.g., "gpt-4o") + +## Security Notes + +⚠️ **Important Security Considerations:** + +- Never commit your real private keys to version control +- Use environment variables or secure secret management for production +- The `.env` file is gitignored by default +- The example private key in `.env.example` is from Anvil and should NEVER be used in production +- Ensure your OpenAI API key is kept secure and not exposed in logs or error messages +- Run the oracle in a secure environment with proper access controls diff --git a/deployments/localhost.json b/deployments/localhost.json new file mode 100644 index 0000000..e78cdce --- /dev/null +++ b/deployments/localhost.json @@ -0,0 +1,15 @@ +{ + "network": "localhost", + "chainId": 31337, + "rpcUrl": "http://localhost:8545", + "deployedAt": "2025-12-10T09:13:15.595Z", + "deployer": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "addresses": { + "easSchemaRegistry": "0x5fbdb2315678afecb367f032d93f642f64180aa3", + "eas": "0xe7f1725e7734ce288f8367e1bb143e90bb3f0512", + "trustedOracleArbiter": "0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0", + "stringObligation": "0xcf7ed3acca5a467e9e704c703e8d87f634fb0fc9", + "erc20EscrowObligation": "0xdc64a140aa3e981100a9beca4e685f962f0cf6c9", + "erc20PaymentObligation": "0x5fc8d32690cc91d4c39d9d3abcbd16989f875707" + } +} \ No newline at end of file diff --git a/package.json b/package.json index 73b0ac2..fa9051a 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,11 @@ "scripts": { "dev": "bun run index.ts", "start": "bun run index.ts", - "test": "bun test ./tests --exclude alkahest-ts/** " + "test": "bun test ./tests --exclude alkahest-ts/** ", + "setup": "./scripts/dev.sh", + "deploy": "./scripts/deploy.sh", + "oracle": "./scripts/start-oracle.sh", + "stop": "./scripts/stop.sh" }, "peerDependencies": { "typescript": "^5.9.3" diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100755 index 0000000..ddb5081 --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,64 @@ +#!/bin/bash + +# Deploy Alkahest contracts to blockchain +# Usage: ./deploy.sh [network] [rpc-url] + +set -e # Exit on error + +# Colors for output +GREEN='\033[0;32m' +BLUE='\033[0;34m' +RED='\033[0;31m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Default values +NETWORK=${1:-localhost} +RPC_URL=${2:-http://localhost:8545} + +echo -e "${BLUE}🚀 Deploying Alkahest Contracts${NC}\n" + +# Check if deployer private key is set +if [ -z "$DEPLOYER_PRIVATE_KEY" ]; then + echo -e "${YELLOW}⚠️ DEPLOYER_PRIVATE_KEY not set, using default Anvil key${NC}" + export DEPLOYER_PRIVATE_KEY="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" +fi + +# Check if alkahest exists +if [ ! -d "../alkahest" ]; then + echo -e "${RED}❌ Error: alkahest repository not found in parent directory${NC}" + echo "Please clone it first: git clone https://github.com/arkhai-io/alkahest.git" + exit 1 +fi + +# Check if contract artifacts exist +if [ ! -d "../alkahest/sdks/ts/src/contracts" ]; then + echo -e "${RED}❌ Error: Contract artifacts not found${NC}" + echo "Expected path: ../alkahest/sdks/ts/src/contracts" + exit 1 +fi + +echo -e "${GREEN}✅ Contract artifacts found${NC}\n" + +# Run deployment +echo -e "${BLUE}📝 Deploying to ${NETWORK}...${NC}" +bun run setups/deploy.ts --network "$NETWORK" --rpc-url "$RPC_URL" + +# Check if deployment was successful +if [ $? -eq 0 ]; then + echo -e "\n${GREEN}✨ Deployment complete!${NC}" + echo -e "${BLUE}Deployment file saved to: deployments/${NETWORK}.json${NC}\n" + + echo -e "${YELLOW}Next steps:${NC}" + echo "1. Start the oracle with:" + echo " ./scripts/start-oracle.sh $NETWORK" + echo "" + echo "2. Or using npm script:" + echo " bun run oracle" + echo "" + echo "3. Or manually:" + echo " bun run setups/oracle.ts --deployment ./deployments/${NETWORK}.json" +else + echo -e "\n${RED}❌ Deployment failed${NC}" + exit 1 +fi diff --git a/scripts/dev.sh b/scripts/dev.sh new file mode 100755 index 0000000..ecca82c --- /dev/null +++ b/scripts/dev.sh @@ -0,0 +1,125 @@ +#!/bin/bash + +# Complete setup and deployment for local development +# This script will: +# 1. Check prerequisites +# 2. Start Anvil +# 3. Deploy contracts +# 4. Start the oracle + +set -e + +# Colors for output +GREEN='\033[0;32m' +BLUE='\033[0;34m' +RED='\033[0;31m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +echo -e "${BLUE}════════════════════════════════════════════════════════${NC}" +echo -e "${BLUE} Natural Language Agreement Oracle - Quick Setup${NC}" +echo -e "${BLUE}════════════════════════════════════════════════════════${NC}\n" + +# Check prerequisites +echo -e "${BLUE}📋 Checking prerequisites...${NC}\n" + +# Load .env file if it exists +if [ -f ".env" ]; then + echo -e "${BLUE}📄 Loading .env file...${NC}" + export $(cat .env | grep -v '^#' | xargs) + echo -e "${GREEN}✅ Environment variables loaded${NC}" +fi + +# Check Bun +if ! command -v bun &> /dev/null; then + echo -e "${RED}❌ Bun is not installed${NC}" + echo "Please install it: https://bun.sh" + exit 1 +fi +echo -e "${GREEN}✅ Bun installed${NC}" + +# Check Foundry +if ! command -v forge &> /dev/null; then + echo -e "${RED}❌ Foundry (forge) is not installed${NC}" + echo "Please install it: https://book.getfoundry.sh/getting-started/installation" + exit 1 +fi +echo -e "${GREEN}✅ Foundry installed${NC}" + +# Check Anvil +if ! command -v anvil &> /dev/null; then + echo -e "${RED}❌ Anvil is not installed${NC}" + echo "Please install Foundry: https://book.getfoundry.sh/getting-started/installation" + exit 1 +fi +echo -e "${GREEN}✅ Anvil installed${NC}" + +# Check alkahest +if [ ! -d "../alkahest" ]; then + echo -e "${RED}❌ alkahest repository not found${NC}" + echo "Please clone it in the parent directory:" + echo " cd .. && git clone https://github.com/arkhai-io/alkahest.git" + exit 1 +fi +echo -e "${GREEN}✅ alkahest repository found${NC}" + +# Check OpenAI API key +if [ -z "$OPENAI_API_KEY" ]; then + echo -e "${RED}❌ OPENAI_API_KEY not set${NC}" + echo "Please create a .env file with: OPENAI_API_KEY=sk-your-key-here" + echo "Or export it: export OPENAI_API_KEY=sk-your-key-here" + exit 1 +fi +echo -e "${GREEN}✅ OpenAI API key configured${NC}\n" + +# Install dependencies +echo -e "${BLUE}📦 Installing dependencies...${NC}\n" +bun install + +# Check if Anvil is already running +if lsof -Pi :8545 -sTCP:LISTEN -t >/dev/null ; then + echo -e "${YELLOW}⚠️ Anvil is already running on port 8545${NC}" + read -p "Do you want to kill it and start fresh? (y/n) " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + pkill -f anvil || true + sleep 2 + else + echo -e "${BLUE}Using existing Anvil instance${NC}\n" + fi +fi + +# Start Anvil in background if not running +if ! lsof -Pi :8545 -sTCP:LISTEN -t >/dev/null ; then + echo -e "${BLUE}🔨 Starting Anvil...${NC}" + anvil > anvil.log 2>&1 & + ANVIL_PID=$! + echo $ANVIL_PID > .anvil.pid + echo -e "${GREEN}✅ Anvil started (PID: $ANVIL_PID)${NC}" + echo " Logs: tail -f anvil.log" + sleep 3 +fi + +# Deploy contracts +echo -e "\n${BLUE}📝 Deploying contracts...${NC}\n" +export DEPLOYER_PRIVATE_KEY="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" +./scripts/deploy.sh localhost http://localhost:8545 + +# Start oracle +echo -e "\n${BLUE}🚀 Starting oracle...${NC}\n" +export ORACLE_PRIVATE_KEY="0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d" +./scripts/start-oracle.sh localhost + +# Cleanup function +cleanup() { + echo -e "\n${YELLOW}🛑 Shutting down...${NC}" + if [ -f .anvil.pid ]; then + ANVIL_PID=$(cat .anvil.pid) + kill $ANVIL_PID 2>/dev/null || true + rm .anvil.pid + echo -e "${GREEN}✅ Anvil stopped${NC}" + fi + exit 0 +} + +trap cleanup SIGINT SIGTERM diff --git a/scripts/start-oracle.sh b/scripts/start-oracle.sh new file mode 100755 index 0000000..71a804f --- /dev/null +++ b/scripts/start-oracle.sh @@ -0,0 +1,60 @@ +#!/bin/bash + +# Start the Natural Language Agreement Oracle +# Usage: ./start-oracle.sh [network] + +set -e # Exit on error + +# Colors for output +GREEN='\033[0;32m' +BLUE='\033[0;34m' +RED='\033[0;31m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Load .env file if it exists +if [ -f ".env" ]; then + export $(cat .env | grep -v '^#' | xargs) +fi + +# Default network +NETWORK=${1:-localhost} +DEPLOYMENT_FILE="./deployments/${NETWORK}.json" + +echo -e "${BLUE}🚀 Starting Natural Language Agreement Oracle${NC}\n" + +# Check if deployment file exists +if [ ! -f "$DEPLOYMENT_FILE" ]; then + echo -e "${RED}❌ Error: Deployment file not found: $DEPLOYMENT_FILE${NC}" + echo "Please deploy contracts first:" + echo " ./scripts/deploy.sh $NETWORK" + exit 1 +fi + +# Check if OpenAI API key is set +if [ -z "$OPENAI_API_KEY" ]; then + echo -e "${RED}❌ Error: OPENAI_API_KEY environment variable is not set${NC}" + echo "Please set it:" + echo " export OPENAI_API_KEY=sk-your-key-here" + exit 1 +fi + +# Check if Oracle private key is set +if [ -z "$ORACLE_PRIVATE_KEY" ]; then + echo -e "${YELLOW}⚠️ ORACLE_PRIVATE_KEY not set, using default Anvil key${NC}" + export ORACLE_PRIVATE_KEY="0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d" +fi + +# Display configuration +echo -e "${GREEN}Configuration:${NC}" +echo " Network: $NETWORK" +echo " Deployment: $DEPLOYMENT_FILE" +echo " Oracle Key: ${ORACLE_PRIVATE_KEY:0:6}...${ORACLE_PRIVATE_KEY: -4}" +echo "" + +# Start oracle +echo -e "${BLUE}👂 Starting oracle (Press Ctrl+C to stop)...${NC}\n" +bun run setups/oracle.ts \ + --deployment "$DEPLOYMENT_FILE" \ + --openai-api-key "$OPENAI_API_KEY" \ + --private-key "$ORACLE_PRIVATE_KEY" diff --git a/scripts/stop.sh b/scripts/stop.sh new file mode 100755 index 0000000..7536c5d --- /dev/null +++ b/scripts/stop.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +# Stop all running oracle and Anvil processes + +# Colors for output +GREEN='\033[0;32m' +BLUE='\033[0;34m' +RED='\033[0;31m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +echo -e "${BLUE}🛑 Stopping Natural Language Agreement services...${NC}\n" + +# Stop Anvil +if [ -f .anvil.pid ]; then + ANVIL_PID=$(cat .anvil.pid) + if kill -0 $ANVIL_PID 2>/dev/null; then + kill $ANVIL_PID + echo -e "${GREEN}✅ Stopped Anvil (PID: $ANVIL_PID)${NC}" + fi + rm .anvil.pid +else + # Try to kill any running anvil + pkill -f anvil && echo -e "${GREEN}✅ Stopped Anvil${NC}" || echo -e "${YELLOW}⚠️ No Anvil process found${NC}" +fi + +# Stop oracle +pkill -f "bun run oracle" && echo -e "${GREEN}✅ Stopped oracle${NC}" || echo -e "${YELLOW}⚠️ No oracle process found${NC}" + +echo -e "\n${GREEN}✨ Cleanup complete${NC}" diff --git a/setups/deploy.ts b/setups/deploy.ts new file mode 100644 index 0000000..8a6a3cf --- /dev/null +++ b/setups/deploy.ts @@ -0,0 +1,287 @@ +#!/usr/bin/env bun +/** + * Deployment script for Alkahest Natural Language Agreement Oracle + * + * This script deploys all necessary contracts to a blockchain network + * and saves the deployment addresses for use by the oracle CLI. + */ + +import { parseArgs } from "util"; +import { createWalletClient, http, publicActions, parseEther } from "viem"; +import { privateKeyToAccount } from "viem/accounts"; +import { mainnet, sepolia, foundry } from "viem/chains"; +import { writeFileSync, existsSync, mkdirSync } from "fs"; +import { resolve } from "path"; + +// Helper function to display usage +function displayHelp() { + console.log(` +Alkahest Contract Deployment CLI + +Usage: + bun deploy.ts [options] + +Options: + --network Network to deploy to: mainnet, 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: ./deployments/.json) + --help, -h Display this help message + +Environment Variables (alternative to CLI options): + DEPLOYER_PRIVATE_KEY Deployer's private key + RPC_URL Custom RPC URL + +Networks: + mainnet Ethereum Mainnet + sepolia Ethereum Sepolia Testnet + localhost Local development (Anvil/Hardhat) + +Examples: + # Deploy to local Anvil + bun deploy.ts --network localhost --private-key 0x... + + # Deploy to Sepolia + bun deploy.ts --network sepolia --private-key 0x... --rpc-url https://sepolia.infura.io/v3/YOUR-KEY + + # Using environment variables + export DEPLOYER_PRIVATE_KEY=0x... + bun deploy.ts --network localhost +`); +} + +// Parse command line arguments +function parseCliArgs() { + const { values } = parseArgs({ + args: Bun.argv.slice(2), + options: { + "network": { type: "string" }, + "rpc-url": { type: "string" }, + "private-key": { type: "string" }, + "output": { type: "string" }, + "help": { type: "boolean", short: "h" }, + }, + strict: true, + }); + + return values; +} + +// Get chain configuration +function getChain(network: string) { + switch (network.toLowerCase()) { + case "mainnet": + return mainnet; + case "sepolia": + return sepolia; + case "localhost": + case "local": + return foundry; + default: + throw new Error(`Unknown network: ${network}. Use mainnet, sepolia, or localhost`); + } +} + +async function main() { + try { + const args = parseCliArgs(); + + // Display help if requested + if (args.help) { + displayHelp(); + process.exit(0); + } + + // Get configuration + const network = args.network; + const privateKey = args["private-key"] || process.env.DEPLOYER_PRIVATE_KEY; + let rpcUrl = args["rpc-url"] || process.env.RPC_URL; + + // Validate required parameters + if (!network) { + console.error("❌ Error: Network is required. Use --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 DEPLOYER_PRIVATE_KEY"); + console.error("Run with --help for usage information."); + process.exit(1); + } + + // Get chain config + const chain = getChain(network); + + // Set default RPC URL if not provided + if (!rpcUrl) { + if (network === "localhost") { + rpcUrl = "http://localhost:8545"; + } else { + console.error("❌ Error: RPC URL is required for non-localhost networks. Use --rpc-url"); + process.exit(1); + } + } + + console.log("🚀 Starting Alkahest Contract Deployment\n"); + console.log("Configuration:"); + console.log(` 🌐 Network: ${network}`); + console.log(` 📡 RPC URL: ${rpcUrl}`); + console.log(` 🔑 Deployer: ${privateKey.slice(0, 6)}...${privateKey.slice(-4)}\n`); + + // Create deployer account and client + const account = privateKeyToAccount(privateKey as `0x${string}`); + const client = createWalletClient({ + account, + chain, + transport: http(rpcUrl), + }).extend(publicActions); + + console.log(`✅ Deployer address: ${account.address}\n`); + + // Check balance + const balance = await client.getBalance({ address: account.address }); + console.log(`💰 Deployer balance: ${parseFloat((balance / 10n ** 18n).toString()).toFixed(4)} ETH\n`); + + if (balance === 0n) { + console.error("❌ Error: Deployer has no ETH. Please fund the account first."); + process.exit(1); + } + + // Import contract artifacts from alkahest + console.log("📦 Loading contract artifacts...\n"); + + // This requires alkahest to be properly set up + const alkahestPath = "../../alkahest/sdks/ts"; + const contractsPath = `${alkahestPath}/src/contracts`; + + // Import necessary artifacts + const EAS = await import(`${alkahestPath}/tests/fixtures/EAS.json`); + const SchemaRegistry = await import(`${alkahestPath}/tests/fixtures/SchemaRegistry.json`); + const TrustedOracleArbiter = await import(`${contractsPath}/TrustedOracleArbiter.json`); + const StringObligation = await import(`${contractsPath}/StringObligation.json`); + const ERC20EscrowObligation = await import(`${contractsPath}/ERC20EscrowObligation.json`); + const ERC20PaymentObligation = await import(`${contractsPath}/ERC20PaymentObligation.json`); + + console.log("✅ Contract artifacts loaded\n"); + + // Deployment addresses + const addresses: Record = {}; + + // Helper to deploy contracts + async function deployContract( + name: string, + abi: any, + bytecode: string, + args: any[] = [] + ): Promise { + console.log(`📝 Deploying ${name}...`); + + const hash = await client.deployContract({ + abi, + bytecode: bytecode as `0x${string}`, + args, + }); + + console.log(` Transaction: ${hash}`); + const receipt = await client.waitForTransactionReceipt({ hash }); + + if (!receipt.contractAddress) { + throw new Error(`Failed to deploy ${name}`); + } + + console.log(` ✅ Deployed at: ${receipt.contractAddress}\n`); + return receipt.contractAddress; + } + + // Deploy core contracts + console.log("🏗️ Deploying core contracts...\n"); + + addresses.easSchemaRegistry = await deployContract( + "EAS Schema Registry", + SchemaRegistry.abi, + SchemaRegistry.bytecode.object + ); + + addresses.eas = await deployContract( + "EAS", + EAS.abi, + EAS.bytecode.object, + [addresses.easSchemaRegistry] + ); + + // Deploy arbiters + console.log("⚖️ Deploying arbiters...\n"); + + addresses.trustedOracleArbiter = await deployContract( + "Trusted Oracle Arbiter", + TrustedOracleArbiter.abi, + TrustedOracleArbiter.bytecode.object, + [addresses.eas] + ); + + // Deploy obligations + console.log("📋 Deploying obligations...\n"); + + addresses.stringObligation = await deployContract( + "String Obligation", + StringObligation.abi, + StringObligation.bytecode.object, + [addresses.eas, addresses.easSchemaRegistry] + ); + + addresses.erc20EscrowObligation = await deployContract( + "ERC20 Escrow Obligation", + ERC20EscrowObligation.abi, + ERC20EscrowObligation.bytecode.object, + [addresses.eas, addresses.easSchemaRegistry] + ); + + addresses.erc20PaymentObligation = await deployContract( + "ERC20 Payment Obligation", + ERC20PaymentObligation.abi, + ERC20PaymentObligation.bytecode.object, + [addresses.eas, addresses.easSchemaRegistry] + ); + + // Save deployment addresses + const outputPath = args.output || resolve(`./deployments/${network}.json`); + const outputDir = resolve(outputPath, ".."); + + if (!existsSync(outputDir)) { + mkdirSync(outputDir, { recursive: true }); + } + + const deployment = { + network, + chainId: chain.id, + rpcUrl, + deployedAt: new Date().toISOString(), + deployer: account.address, + addresses, + }; + + writeFileSync(outputPath, JSON.stringify(deployment, null, 2)); + + console.log("✨ Deployment complete!\n"); + console.log("📄 Deployment details saved to:", outputPath); + console.log("\n📋 Deployed Addresses:"); + Object.entries(addresses).forEach(([name, address]) => { + console.log(` ${name}: ${address}`); + }); + + console.log("\n🎯 Next steps:"); + console.log("1. Save the deployment file for your records"); + console.log("2. Update your oracle configuration with the EAS contract address:"); + console.log(` export EAS_CONTRACT_ADDRESS=${addresses.eas}`); + console.log("3. Start the oracle:"); + console.log(` bun run oracle -- --rpc-url ${rpcUrl} --eas-contract ${addresses.eas}`); + + } catch (error) { + console.error("❌ Deployment failed:", error); + process.exit(1); + } +} + +// Run the deployment +main(); diff --git a/setups/oracle.ts b/setups/oracle.ts new file mode 100644 index 0000000..5f65001 --- /dev/null +++ b/setups/oracle.ts @@ -0,0 +1,243 @@ +#!/usr/bin/env bun +import { parseArgs } from "util"; +import { parseAbiParameters } from "viem"; +import { makeLLMClient } from "../clients/nla"; +import { existsSync, readFileSync } from "fs"; +import { resolve } from "path"; + +// Helper function to display usage +function displayHelp() { + console.log(` +Natural Language Agreement Oracle CLI + +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 (required) + --eas-contract
EAS contract address (optional) + --deployment Load addresses from deployment file (optional) + --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 + ORACLE_PRIVATE_KEY Private key of the oracle operator + OPENAI_API_KEY OpenAI API key + 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/localhost.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... + bun oracle.ts +`); +} + +// Parse command line arguments +function parseCliArgs() { + const { values } = parseArgs({ + args: Bun.argv.slice(2), + options: { + "rpc-url": { type: "string" }, + "private-key": { type: "string" }, + "openai-api-key": { type: "string" }, + "eas-contract": { type: "string" }, + "deployment": { type: "string" }, + "polling-interval": { type: "string" }, + "help": { type: "boolean", short: "h" }, + }, + strict: true, + }); + + 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 { + const args = parseCliArgs(); + + // Display help if requested + if (args.help) { + displayHelp(); + 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`); + } + + const privateKey = args["private-key"] || process.env.ORACLE_PRIVATE_KEY; + const openaiApiKey = args["openai-api-key"] || process.env.OPENAI_API_KEY; + 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."); + 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."); + process.exit(1); + } + + if (!openaiApiKey) { + console.error("❌ Error: OpenAI API key is required. Use --openai-api-key or set OPENAI_API_KEY environment variable."); + console.error("Run with --help for usage information."); + process.exit(1); + } + + // Import alkahest client + const { makeClient } = await import("../../alkahest/sdks/ts/src/index.ts"); + const { createWalletClient, http, publicActions } = await import("viem"); + const { privateKeyToAccount } = await import("viem/accounts"); + const { foundry } = await import("viem/chains"); + + 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)}`); + console.log(` 🤖 AI Provider: OpenAI`); + if (easContract) { + console.log(` 📝 EAS Contract: ${easContract}`); + } + console.log(` ⏱️ Polling Interval: ${pollingInterval}ms\n`); + + // Create wallet client + const account = privateKeyToAccount(privateKey as `0x${string}`); + const walletClient = createWalletClient({ + account, + chain: foundry, + transport: http(rpcUrl), + }).extend(publicActions) as any; + + // Create alkahest client + const client = makeClient(walletClient, deploymentAddresses || { eas: easContract }); + + console.log(`✅ Oracle initialized with address: ${account.address}\n`); + + // Create LLM client + const llmClient = client.extend(() => ({ + llm: makeLLMClient([]), + })); + + llmClient.llm.addProvider({ + providerName: "OpenAI", + apiKey: openaiApiKey, + }); + + console.log("🎯 LLM Arbitrator configured and ready\n"); + console.log("👂 Listening for arbitration requests...\n"); + + // Define the obligation ABI + const obligationAbi = parseAbiParameters("(string item)"); + + // Start listening and arbitrating + const { unwatch } = await client.oracle.listenAndArbitrate( + async (attestation: any) => { + console.log(`\n📨 New arbitration request received!`); + console.log(` Attestation UID: ${attestation.uid}`); + + try { + // Extract obligation data + const obligation = client.extractObligationData( + obligationAbi, + attestation, + ); + console.log(` Obligation: "${obligation[0].item}"`); + + // Get demand data + const [, demand] = await client.getEscrowAndDemand( + llmClient.llm.LLMAbi, + attestation, + ); + console.log(` Demand: "${demand[0].demand}"`); + console.log(` Model: ${demand[0].arbitrationModel}`); + + // Perform arbitration using LLM + console.log(` 🤔 Arbitrating with AI...`); + const result = await llmClient.llm.arbitrate( + demand[0], + obligation[0].item + ); + + console.log(` ✨ Arbitration result: ${result ? "✅ APPROVED" : "❌ REJECTED"}`); + return result; + } catch (error) { + console.error(` ❌ Error during arbitration:`, error); + throw error; + } + }, + { + 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`); + }, + pollingInterval, + }, + ); + + console.log("✨ Oracle is now running. Press Ctrl+C to stop.\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();