Merge pull request #4 from arkhai-io/refactor
Feat: switch network, public npm package
This commit is contained in:
commit
9b83e5c3d7
|
|
@ -41,4 +41,4 @@ anvil.log
|
|||
.env
|
||||
|
||||
# Local deployment files
|
||||
cli/deployments/localhost.json
|
||||
cli/deployments/devnet.json
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
# Source files (only compiled JS will be included)
|
||||
*.ts
|
||||
!*.d.ts
|
||||
|
||||
# Tests
|
||||
tests/
|
||||
**/*.test.ts
|
||||
**/*.test.js
|
||||
|
||||
# Build config
|
||||
tsconfig.json
|
||||
tsconfig.build.json
|
||||
bun.lockb
|
||||
|
||||
# Development
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
anvil.log
|
||||
.anvil.pid
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Git
|
||||
.git/
|
||||
.gitignore
|
||||
|
||||
# CI/CD
|
||||
.github/
|
||||
|
||||
# Documentation (keep README.md)
|
||||
docs/
|
||||
|
||||
# Deployments
|
||||
deployments/
|
||||
|
||||
# Node modules (will be installed by user)
|
||||
node_modules/
|
||||
|
||||
# Parent directory references
|
||||
../
|
||||
|
|
@ -0,0 +1,173 @@
|
|||
# Installing NLA CLI Globally
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js >= 18.0.0
|
||||
- npm or yarn or pnpm
|
||||
- A blockchain node (local Anvil or remote RPC URL)
|
||||
- At least one LLM provider API key (OpenAI, Anthropic, or OpenRouter)
|
||||
|
||||
## Installation
|
||||
|
||||
### Global Installation (Recommended)
|
||||
|
||||
Install the NLA CLI globally to use it from anywhere:
|
||||
|
||||
```bash
|
||||
npm install -g nla
|
||||
```
|
||||
|
||||
Or with yarn:
|
||||
```bash
|
||||
yarn global add nla
|
||||
```
|
||||
|
||||
Or with pnpm:
|
||||
```bash
|
||||
pnpm add -g nla
|
||||
```
|
||||
|
||||
### Verify Installation
|
||||
|
||||
```bash
|
||||
nla --help
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Create a `.env` file in your project directory:
|
||||
|
||||
```bash
|
||||
# Copy the example environment file
|
||||
cp node_modules/nla/.env.example .env
|
||||
|
||||
# Edit with your configuration
|
||||
nano .env
|
||||
```
|
||||
|
||||
Required environment variables:
|
||||
```bash
|
||||
# At least one LLM provider API key
|
||||
OPENAI_API_KEY=sk-...
|
||||
# OR
|
||||
ANTHROPIC_API_KEY=sk-ant-...
|
||||
# OR
|
||||
OPENROUTER_API_KEY=sk-or-...
|
||||
|
||||
# Oracle configuration
|
||||
ORACLE_PRIVATE_KEY=0x...
|
||||
RPC_URL=http://localhost:8545
|
||||
|
||||
# Optional: For enhanced search
|
||||
PERPLEXITY_API_KEY=pplx-...
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Start Development Environment
|
||||
|
||||
```bash
|
||||
nla dev
|
||||
```
|
||||
|
||||
This will:
|
||||
- Start Anvil (local blockchain)
|
||||
- Deploy all contracts
|
||||
- Start the oracle
|
||||
|
||||
### 2. Create an Escrow
|
||||
|
||||
```bash
|
||||
nla escrow:create \
|
||||
--demand "The sky is blue" \
|
||||
--amount 10 \
|
||||
--token 0xa513E6E4b8f2a923D98304ec87F64353C4D5C853 \
|
||||
--oracle 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
|
||||
```
|
||||
|
||||
### 3. Fulfill an Escrow
|
||||
|
||||
```bash
|
||||
nla escrow:fulfill \
|
||||
--escrow-uid 0x... \
|
||||
--fulfillment "The sky appears blue today" \
|
||||
--oracle 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
|
||||
```
|
||||
|
||||
### 4. Collect Payment
|
||||
|
||||
```bash
|
||||
nla escrow:collect \
|
||||
--escrow-uid 0x... \
|
||||
--fulfillment-uid 0x...
|
||||
```
|
||||
|
||||
## Uninstallation
|
||||
|
||||
```bash
|
||||
npm uninstall -g nla
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
If you want to contribute or modify the CLI:
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/arkhai-io/natural-language-agreements.git
|
||||
cd natural-language-agreements
|
||||
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Build
|
||||
npm run build
|
||||
|
||||
# Link locally
|
||||
npm link
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Command not found after installation
|
||||
|
||||
Make sure your npm global bin directory is in your PATH:
|
||||
|
||||
```bash
|
||||
# Check npm global bin path
|
||||
npm bin -g
|
||||
|
||||
# Add to PATH (add to ~/.bashrc or ~/.zshrc)
|
||||
export PATH="$(npm bin -g):$PATH"
|
||||
```
|
||||
|
||||
### Permission errors on Linux/Mac
|
||||
|
||||
If you get permission errors, either:
|
||||
|
||||
1. Use a Node version manager (recommended):
|
||||
```bash
|
||||
# Install nvm
|
||||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
|
||||
|
||||
# Install and use Node
|
||||
nvm install 18
|
||||
nvm use 18
|
||||
|
||||
# Now install without sudo
|
||||
npm install -g nla
|
||||
```
|
||||
|
||||
2. Or configure npm to use a different directory:
|
||||
```bash
|
||||
mkdir ~/.npm-global
|
||||
npm config set prefix '~/.npm-global'
|
||||
echo 'export PATH=~/.npm-global/bin:$PATH' >> ~/.bashrc
|
||||
source ~/.bashrc
|
||||
```
|
||||
|
||||
## Support
|
||||
|
||||
For issues and questions:
|
||||
- GitHub: https://github.com/arkhai-io/natural-language-agreements/issues
|
||||
- Documentation: https://github.com/arkhai-io/natural-language-agreements
|
||||
|
|
@ -79,7 +79,7 @@ export OPENAI_API_KEY=sk-your-key-here
|
|||
nla deploy
|
||||
```
|
||||
|
||||
This creates `cli/deployments/localhost.json` with all contract addresses.
|
||||
This creates `cli/deployments/devnet.json` with all contract addresses.
|
||||
|
||||
#### 3. Start Oracle
|
||||
|
||||
|
|
@ -316,7 +316,7 @@ natural-language-agreements/
|
|||
│ │ ├── start-oracle.sh # Oracle starter
|
||||
│ │ └── stop.sh # Cleanup script
|
||||
│ └── deployments/ # Deployment addresses (generated)
|
||||
│ ├── localhost.json
|
||||
│ ├── devnet.json
|
||||
│ ├── sepolia.json
|
||||
│ └── mainnet.json
|
||||
├── nla.ts # Natural Language Agreement client library
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ Monitor the escrow and arbitration progress:
|
|||
```bash
|
||||
nla escrow:status \
|
||||
--escrow-uid 0x... \
|
||||
--deployment ./cli/deployments/localhost.json
|
||||
--deployment ./cli/deployments/devnet.json
|
||||
```
|
||||
|
||||
This will show:
|
||||
|
|
@ -185,7 +185,7 @@ Options:
|
|||
--token <address> ERC20 token address (required)
|
||||
--oracle <address> Oracle address (required)
|
||||
--private-key <key> Your private key (or set PRIVATE_KEY env var)
|
||||
--deployment <path> Deployment file (default: ./cli/deployments/localhost.json)
|
||||
--deployment <path> Deployment file (default: ./cli/deployments/devnet.json)
|
||||
--rpc-url <url> RPC URL (default: from deployment)
|
||||
--help, -h Show help
|
||||
```
|
||||
|
|
@ -200,7 +200,7 @@ Options:
|
|||
--fulfillment <text> Your fulfillment text (required)
|
||||
--oracle <address> Oracle address (required)
|
||||
--private-key <key> Your private key (or set PRIVATE_KEY env var)
|
||||
--deployment <path> Deployment file (default: ./cli/deployments/localhost.json)
|
||||
--deployment <path> Deployment file (default: ./cli/deployments/devnet.json)
|
||||
--rpc-url <url> RPC URL (default: from deployment)
|
||||
--help, -h Show help
|
||||
```
|
||||
|
|
@ -214,7 +214,7 @@ Options:
|
|||
--escrow-uid <uid> Escrow UID (required)
|
||||
--fulfillment-uid <uid> Approved fulfillment UID (required)
|
||||
--private-key <key> Your private key (or set PRIVATE_KEY env var)
|
||||
--deployment <path> Deployment file (default: ./cli/deployments/localhost.json)
|
||||
--deployment <path> Deployment file (default: ./cli/deployments/devnet.json)
|
||||
--rpc-url <url> RPC URL (default: from deployment)
|
||||
--help, -h Show help
|
||||
```
|
||||
|
|
@ -226,7 +226,7 @@ nla escrow:status [options]
|
|||
|
||||
Options:
|
||||
--escrow-uid <uid> Escrow UID to check (required)
|
||||
--deployment <path> Deployment file (default: ./cli/deployments/localhost.json)
|
||||
--deployment <path> Deployment file (default: ./cli/deployments/devnet.json)
|
||||
--rpc-url <url> RPC URL (default: from deployment)
|
||||
--help, -h Show help
|
||||
```
|
||||
|
|
@ -293,7 +293,7 @@ nla escrow:fulfill \
|
|||
# Terminal 2: Check the status
|
||||
nla escrow:status \
|
||||
--escrow-uid 0xd9e1402e96c2f7a64e60bf53a45445f7254e9b72389f6ede25181bff542d7b65 \
|
||||
--deployment ./cli/deployments/localhost.json
|
||||
--deployment ./cli/deployments/devnet.json
|
||||
|
||||
# Terminal 2: Collect the escrow (as Bob)
|
||||
nla escrow:collect \
|
||||
|
|
|
|||
|
|
@ -8,10 +8,43 @@
|
|||
import { parseArgs } from "util";
|
||||
import { createWalletClient, http, publicActions } from "viem";
|
||||
import { privateKeyToAccount } from "viem/accounts";
|
||||
import { foundry } from "viem/chains";
|
||||
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 { getChainFromNetwork } 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() {
|
||||
|
|
@ -27,7 +60,7 @@ Options:
|
|||
--escrow-uid <uid> Escrow UID to collect (required)
|
||||
--fulfillment-uid <uid> Fulfillment UID that was approved (required)
|
||||
--private-key <key> Your private key (required)
|
||||
--deployment <path> Path to deployment file (default: ./cli/deployments/localhost.json)
|
||||
--deployment <path> Path to deployment file (default: ./cli/deployments/devnet.json)
|
||||
--rpc-url <url> RPC URL (default: from deployment file)
|
||||
--help, -h Display this help message
|
||||
|
||||
|
|
@ -80,7 +113,7 @@ async function main() {
|
|||
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/localhost.json";
|
||||
const deploymentPath = args.deployment || "./cli/deployments/devnet.json";
|
||||
|
||||
// Validate required parameters
|
||||
if (!escrowUid) {
|
||||
|
|
@ -102,14 +135,20 @@ async function main() {
|
|||
}
|
||||
|
||||
// Load deployment file
|
||||
if (!existsSync(resolve(deploymentPath))) {
|
||||
const resolvedDeploymentPath = findDeploymentFile(deploymentPath);
|
||||
if (!resolvedDeploymentPath) {
|
||||
console.error(`❌ Error: Deployment file not found: ${deploymentPath}`);
|
||||
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(resolve(deploymentPath), "utf-8"));
|
||||
const deployment = JSON.parse(readFileSync(resolvedDeploymentPath, "utf-8"));
|
||||
const rpcUrl = args["rpc-url"] || deployment.rpcUrl;
|
||||
const chain = getChainFromNetwork(deployment.network);
|
||||
|
||||
console.log("🚀 Collecting Natural Language Agreement Escrow\n");
|
||||
console.log("Configuration:");
|
||||
|
|
@ -121,7 +160,7 @@ async function main() {
|
|||
const account = privateKeyToAccount(privateKey as `0x${string}`);
|
||||
const walletClient = createWalletClient({
|
||||
account,
|
||||
chain: foundry,
|
||||
chain,
|
||||
transport: http(rpcUrl),
|
||||
}).extend(publicActions);
|
||||
|
||||
|
|
|
|||
|
|
@ -9,20 +9,64 @@
|
|||
import { parseArgs } from "util";
|
||||
import { createWalletClient, http, publicActions, parseEther } from "viem";
|
||||
import { privateKeyToAccount } from "viem/accounts";
|
||||
import { foundry } from "viem/chains";
|
||||
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 { makeLLMClient } from "../..";
|
||||
import {fixtures} from "alkahest-ts";
|
||||
import { getCurrentEnvironment } from "../commands/switch.js";
|
||||
import { getChainFromNetwork } 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();
|
||||
console.log(`
|
||||
Natural Language Agreement Escrow CLI
|
||||
|
||||
Create an escrow with a natural language demand that will be arbitrated by an oracle.
|
||||
|
||||
Current environment: ${currentEnv}
|
||||
|
||||
Usage:
|
||||
bun cli/create-escrow.ts [options]
|
||||
|
||||
|
|
@ -32,7 +76,7 @@ Options:
|
|||
--token <address> ERC20 token address (required)
|
||||
--oracle <address> Oracle address that will arbitrate (required)
|
||||
--private-key <key> Your private key (required)
|
||||
--deployment <path> Path to deployment file (default: ./cli/deployments/localhost.json)
|
||||
--deployment <path> Path to deployment file (default: current environment)
|
||||
--rpc-url <url> RPC URL (default: from deployment file)
|
||||
--arbitration-provider <name> Arbitration provider (default: OpenAI)
|
||||
--arbitration-model <model> Arbitration model (default: gpt-4o-mini)
|
||||
|
|
@ -97,7 +141,7 @@ async function main() {
|
|||
const tokenAddress = args.token;
|
||||
const oracleAddress = args.oracle;
|
||||
const privateKey = args["private-key"] || process.env.PRIVATE_KEY;
|
||||
const deploymentPath = args.deployment || "./cli/deployments/localhost.json";
|
||||
const deploymentPath = args.deployment || `./cli/deployments/${getCurrentEnvironment()}.json`;
|
||||
|
||||
// Arbitration configuration with defaults
|
||||
const arbitrationProvider = args["arbitration-provider"] || "OpenAI";
|
||||
|
|
@ -141,14 +185,20 @@ Fulfillment: {{obligation}}`;
|
|||
}
|
||||
|
||||
// Load deployment file
|
||||
if (!existsSync(resolve(deploymentPath))) {
|
||||
const resolvedDeploymentPath = findDeploymentFile(deploymentPath);
|
||||
if (!resolvedDeploymentPath) {
|
||||
console.error(`❌ Error: Deployment file not found: ${deploymentPath}`);
|
||||
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(resolve(deploymentPath), "utf-8"));
|
||||
const deployment = JSON.parse(readFileSync(resolvedDeploymentPath, "utf-8"));
|
||||
const rpcUrl = args["rpc-url"] || deployment.rpcUrl;
|
||||
const chain = getChainFromNetwork(deployment.network);
|
||||
|
||||
console.log("🚀 Creating Natural Language Agreement Escrow\n");
|
||||
console.log("Configuration:");
|
||||
|
|
@ -156,13 +206,14 @@ Fulfillment: {{obligation}}`;
|
|||
console.log(` 💰 Amount: ${amount} tokens`);
|
||||
console.log(` 🪙 Token: ${tokenAddress}`);
|
||||
console.log(` ⚖️ Oracle: ${oracleAddress}`);
|
||||
console.log(` 🌐 Network: ${deployment.network}`);
|
||||
console.log(` 🌐 RPC URL: ${rpcUrl}\n`);
|
||||
|
||||
// Create account and wallet
|
||||
const account = privateKeyToAccount(privateKey as `0x${string}`);
|
||||
const walletClient = createWalletClient({
|
||||
account,
|
||||
chain: foundry,
|
||||
chain,
|
||||
transport: http(rpcUrl),
|
||||
}).extend(publicActions);
|
||||
|
||||
|
|
|
|||
|
|
@ -9,11 +9,44 @@
|
|||
import { parseArgs } from "util";
|
||||
import { createWalletClient, http, publicActions } from "viem";
|
||||
import { privateKeyToAccount } from "viem/accounts";
|
||||
import { foundry } from "viem/chains";
|
||||
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 { makeLLMClient } from "../..";
|
||||
import { getChainFromNetwork } 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() {
|
||||
|
|
@ -30,7 +63,7 @@ Options:
|
|||
--fulfillment <text> Your fulfillment text (required)
|
||||
--oracle <address> Oracle address that will arbitrate (required)
|
||||
--private-key <key> Your private key (required)
|
||||
--deployment <path> Path to deployment file (default: ./cli/deployments/localhost.json)
|
||||
--deployment <path> Path to deployment file (default: ./cli/deployments/devnet.json)
|
||||
--rpc-url <url> RPC URL (default: from deployment file)
|
||||
--help, -h Display this help message
|
||||
|
||||
|
|
@ -86,7 +119,7 @@ async function main() {
|
|||
const fulfillment = args.fulfillment;
|
||||
const oracleAddress = args.oracle;
|
||||
const privateKey = args["private-key"] || process.env.PRIVATE_KEY;
|
||||
const deploymentPath = args.deployment || "./cli/deployments/localhost.json";
|
||||
const deploymentPath = args.deployment || "./cli/deployments/devnet.json";
|
||||
|
||||
// Validate required parameters
|
||||
if (!escrowUid) {
|
||||
|
|
@ -114,27 +147,34 @@ async function main() {
|
|||
}
|
||||
|
||||
// Load deployment file
|
||||
if (!existsSync(resolve(deploymentPath))) {
|
||||
const resolvedDeploymentPath = findDeploymentFile(deploymentPath);
|
||||
if (!resolvedDeploymentPath) {
|
||||
console.error(`❌ Error: Deployment file not found: ${deploymentPath}`);
|
||||
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(resolve(deploymentPath), "utf-8"));
|
||||
const deployment = JSON.parse(readFileSync(resolvedDeploymentPath, "utf-8"));
|
||||
const rpcUrl = args["rpc-url"] || deployment.rpcUrl;
|
||||
const chain = getChainFromNetwork(deployment.network);
|
||||
|
||||
console.log("🚀 Fulfilling Natural Language Agreement Escrow\n");
|
||||
console.log("Configuration:");
|
||||
console.log(` 📦 Escrow UID: ${escrowUid}`);
|
||||
console.log(` 📝 Fulfillment: "${fulfillment}"`);
|
||||
console.log(` ⚖️ Oracle: ${oracleAddress}`);
|
||||
console.log(` 🌐 Network: ${deployment.network}`);
|
||||
console.log(` 🌐 RPC URL: ${rpcUrl}\n`);
|
||||
|
||||
// Create account and wallet
|
||||
const account = privateKeyToAccount(privateKey as `0x${string}`);
|
||||
const walletClient = createWalletClient({
|
||||
account,
|
||||
chain: foundry,
|
||||
chain,
|
||||
transport: http(rpcUrl),
|
||||
}).extend(publicActions);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,250 @@
|
|||
import { spawn, spawnSync } from "child_process";
|
||||
import { existsSync, readFileSync, writeFileSync, createWriteStream, unlinkSync } from "fs";
|
||||
import { join } from "path";
|
||||
|
||||
// Colors for console output
|
||||
const colors = {
|
||||
green: '\x1b[32m',
|
||||
blue: '\x1b[34m',
|
||||
red: '\x1b[31m',
|
||||
yellow: '\x1b[33m',
|
||||
reset: '\x1b[0m'
|
||||
};
|
||||
|
||||
// Load .env file if it exists
|
||||
function loadEnvFile(envPath: string = '.env') {
|
||||
if (existsSync(envPath)) {
|
||||
console.log(`${colors.blue}📄 Loading .env file from: ${envPath}${colors.reset}`);
|
||||
const envContent = readFileSync(envPath, 'utf-8');
|
||||
const lines = envContent.split('\n');
|
||||
|
||||
for (const line of lines) {
|
||||
// Skip comments and empty lines
|
||||
if (line.trim().startsWith('#') || !line.trim()) continue;
|
||||
|
||||
// Parse key=value
|
||||
const match = line.match(/^([^=]+)=(.*)$/);
|
||||
if (match) {
|
||||
const key = match[1].trim();
|
||||
const value = match[2].trim();
|
||||
// Only set if not already in environment
|
||||
if (!process.env[key]) {
|
||||
process.env[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(`${colors.green}✅ Environment variables loaded${colors.reset}`);
|
||||
} else if (envPath !== '.env') {
|
||||
// Only error if a custom path was specified but not found
|
||||
console.error(`${colors.red}❌ .env file not found at: ${envPath}${colors.reset}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to check if a command exists
|
||||
function commandExists(command: string): boolean {
|
||||
try {
|
||||
const result = spawnSync(process.platform === 'win32' ? 'where' : 'which', [command], {
|
||||
stdio: 'pipe'
|
||||
});
|
||||
return result.status === 0;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to check if a port is in use
|
||||
function isPortInUse(port: number): boolean {
|
||||
try {
|
||||
const result = spawnSync('lsof', ['-Pi', `:${port}`, '-sTCP:LISTEN', '-t'], {
|
||||
stdio: 'pipe'
|
||||
});
|
||||
return result.status === 0;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Dev command - Start complete development environment
|
||||
export async function runDevCommand(cliDir: string, envPath?: string) {
|
||||
console.log(`${colors.blue}════════════════════════════════════════════════════════${colors.reset}`);
|
||||
console.log(`${colors.blue} Natural Language Agreement Oracle - Quick Setup${colors.reset}`);
|
||||
console.log(`${colors.blue}════════════════════════════════════════════════════════${colors.reset}\n`);
|
||||
|
||||
// Load .env file first
|
||||
loadEnvFile(envPath);
|
||||
console.log('');
|
||||
|
||||
// Check prerequisites
|
||||
console.log(`${colors.blue}📋 Checking prerequisites...${colors.reset}\n`);
|
||||
|
||||
// Check Bun
|
||||
if (!commandExists('bun')) {
|
||||
console.error(`${colors.red}❌ Bun is not installed${colors.reset}`);
|
||||
console.log('Please install it: https://bun.sh');
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`${colors.green}✅ Bun installed${colors.reset}`);
|
||||
|
||||
// Check Foundry
|
||||
if (!commandExists('forge')) {
|
||||
console.error(`${colors.red}❌ Foundry (forge) is not installed${colors.reset}`);
|
||||
console.log('Please install it: https://book.getfoundry.sh/getting-started/installation');
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`${colors.green}✅ Foundry installed${colors.reset}`);
|
||||
|
||||
// Check Anvil
|
||||
if (!commandExists('anvil')) {
|
||||
console.error(`${colors.red}❌ Anvil is not installed${colors.reset}`);
|
||||
console.log('Please install Foundry: https://book.getfoundry.sh/getting-started/installation');
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`${colors.green}✅ Anvil installed${colors.reset}`);
|
||||
|
||||
// Check LLM API keys
|
||||
const hasOpenAI = !!process.env.OPENAI_API_KEY;
|
||||
const hasAnthropic = !!process.env.ANTHROPIC_API_KEY;
|
||||
const hasOpenRouter = !!process.env.OPENROUTER_API_KEY;
|
||||
const hasPerplexity = !!process.env.PERPLEXITY_API_KEY;
|
||||
|
||||
if (!hasOpenAI && !hasAnthropic && !hasOpenRouter && !hasPerplexity) {
|
||||
console.error(`${colors.red}❌ No LLM provider API key set${colors.reset}`);
|
||||
console.log('Please add at least one API key to your environment:');
|
||||
console.log(' export OPENAI_API_KEY=sk-...');
|
||||
console.log(' export ANTHROPIC_API_KEY=sk-ant-...');
|
||||
console.log(' export OPENROUTER_API_KEY=sk-or-...');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (hasOpenAI) console.log(`${colors.green}✅ OpenAI API key configured${colors.reset}`);
|
||||
if (hasAnthropic) console.log(`${colors.green}✅ Anthropic API key configured${colors.reset}`);
|
||||
if (hasOpenRouter) console.log(`${colors.green}✅ OpenRouter API key configured${colors.reset}`);
|
||||
if (hasPerplexity) console.log(`${colors.green}✅ Perplexity API key configured${colors.reset}`);
|
||||
console.log('');
|
||||
|
||||
// Check if Anvil is already running
|
||||
if (isPortInUse(8545)) {
|
||||
console.log(`${colors.yellow}⚠️ Anvil is already running on port 8545${colors.reset}`);
|
||||
console.log(`${colors.blue}Using existing Anvil instance${colors.reset}\n`);
|
||||
} else {
|
||||
// Start Anvil
|
||||
console.log(`${colors.blue}🔨 Starting Anvil...${colors.reset}`);
|
||||
const anvilProcess = spawn('anvil', [], {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
detached: true
|
||||
});
|
||||
|
||||
// Save PID
|
||||
writeFileSync('.anvil.pid', anvilProcess.pid!.toString());
|
||||
|
||||
// Redirect output to log file
|
||||
const logStream = createWriteStream('anvil.log', { flags: 'a' });
|
||||
anvilProcess.stdout?.pipe(logStream);
|
||||
anvilProcess.stderr?.pipe(logStream);
|
||||
|
||||
anvilProcess.unref();
|
||||
|
||||
console.log(`${colors.green}✅ Anvil started (PID: ${anvilProcess.pid})${colors.reset}`);
|
||||
console.log(' Logs: tail -f anvil.log');
|
||||
|
||||
// Wait for Anvil to be ready
|
||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||
}
|
||||
|
||||
// Deploy contracts
|
||||
console.log(`\n${colors.blue}📝 Deploying contracts...${colors.reset}\n`);
|
||||
const deployScript = join(cliDir, 'server', 'deploy.js');
|
||||
const deployResult = spawnSync('bun', ['run', deployScript, '--network', 'localhost', '--rpc-url', 'http://localhost:8545'], {
|
||||
stdio: 'inherit',
|
||||
cwd: process.cwd()
|
||||
});
|
||||
|
||||
if (deployResult.status !== 0) {
|
||||
console.error(`${colors.red}❌ Deployment failed${colors.reset}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Start oracle
|
||||
console.log(`\n${colors.blue}🚀 Starting oracle...${colors.reset}\n`);
|
||||
const oracleScript = join(cliDir, 'server', 'oracle.js');
|
||||
|
||||
// Look for deployment file in source directory (for local dev) or dist directory (for installed package)
|
||||
const sourcePath = join(process.cwd(), 'cli', 'deployments', 'devnet.json');
|
||||
const distPath = join(cliDir, 'deployments', 'devnet.json');
|
||||
const deploymentFile = existsSync(sourcePath) ? sourcePath : distPath;
|
||||
|
||||
const oracleProcess = spawn('bun', ['run', oracleScript, '--deployment', deploymentFile], {
|
||||
stdio: 'inherit',
|
||||
cwd: process.cwd()
|
||||
});
|
||||
|
||||
// Handle cleanup on exit
|
||||
const cleanup = () => {
|
||||
console.log(`\n${colors.yellow}🛑 Shutting down...${colors.reset}`);
|
||||
|
||||
// Kill oracle
|
||||
oracleProcess.kill();
|
||||
|
||||
// Kill Anvil
|
||||
try {
|
||||
const pidFile = '.anvil.pid';
|
||||
if (existsSync(pidFile)) {
|
||||
const pid = readFileSync(pidFile, 'utf-8').trim();
|
||||
try {
|
||||
process.kill(parseInt(pid));
|
||||
console.log(`${colors.green}✅ Anvil stopped${colors.reset}`);
|
||||
} catch (e) {
|
||||
// Process might already be dead
|
||||
}
|
||||
unlinkSync(pidFile);
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore errors
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
process.on('SIGINT', cleanup);
|
||||
process.on('SIGTERM', cleanup);
|
||||
|
||||
// Open a new terminal
|
||||
console.log(`\n${colors.green}✅ Setup complete!${colors.reset}`);
|
||||
console.log(`${colors.blue}Opening new terminal...${colors.reset}\n`);
|
||||
|
||||
try {
|
||||
// For macOS, open a new Terminal window
|
||||
if (process.platform === 'darwin') {
|
||||
spawn('osascript', [
|
||||
'-e',
|
||||
`tell application "Terminal" to do script "cd ${process.cwd()}"`
|
||||
], { detached: true, stdio: 'ignore' }).unref();
|
||||
}
|
||||
// For Linux, try common terminal emulators
|
||||
else if (process.platform === 'linux') {
|
||||
const terminals = ['gnome-terminal', 'konsole', 'xterm'];
|
||||
for (const term of terminals) {
|
||||
if (commandExists(term)) {
|
||||
spawn(term, ['--working-directory', process.cwd()], {
|
||||
detached: true,
|
||||
stdio: 'ignore'
|
||||
}).unref();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// For Windows
|
||||
else if (process.platform === 'win32') {
|
||||
spawn('cmd', ['/c', 'start', 'cmd', '/K', `cd /d ${process.cwd()}`], {
|
||||
detached: true,
|
||||
stdio: 'ignore'
|
||||
}).unref();
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(`${colors.yellow}⚠️ Could not open new terminal automatically${colors.reset}`);
|
||||
}
|
||||
|
||||
// Keep process alive
|
||||
await new Promise(() => {});
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
import { spawnSync } from "child_process";
|
||||
import { existsSync, readFileSync, unlinkSync } from "fs";
|
||||
|
||||
// Colors for console output
|
||||
const colors = {
|
||||
green: '\x1b[32m',
|
||||
yellow: '\x1b[33m',
|
||||
red: '\x1b[31m',
|
||||
reset: '\x1b[0m'
|
||||
};
|
||||
|
||||
// Stop command - Stop all services
|
||||
export async function runStopCommand() {
|
||||
console.log(`${colors.yellow}🛑 Stopping services...${colors.reset}\n`);
|
||||
|
||||
// Stop Anvil
|
||||
try {
|
||||
const pidFile = '.anvil.pid';
|
||||
if (existsSync(pidFile)) {
|
||||
const pid = readFileSync(pidFile, 'utf-8').trim();
|
||||
try {
|
||||
process.kill(parseInt(pid));
|
||||
console.log(`${colors.green}✅ Anvil stopped${colors.reset}`);
|
||||
} catch (e) {
|
||||
console.log(`${colors.yellow}⚠️ Anvil process not found (may have already stopped)${colors.reset}`);
|
||||
}
|
||||
unlinkSync(pidFile);
|
||||
} else {
|
||||
console.log(`${colors.yellow}⚠️ No Anvil PID file found${colors.reset}`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`${colors.red}❌ Error stopping Anvil:${colors.reset}`, e);
|
||||
}
|
||||
|
||||
// Try to kill any remaining processes on port 8545
|
||||
try {
|
||||
spawnSync('pkill', ['-f', 'anvil']);
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
console.log(`\n${colors.green}✅ Services stopped${colors.reset}`);
|
||||
}
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
||||
import { join, dirname } from "path";
|
||||
import { homedir } from "os";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
// Get the directory of the current module
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
// Colors for console output
|
||||
const colors = {
|
||||
green: '\x1b[32m',
|
||||
blue: '\x1b[34m',
|
||||
red: '\x1b[31m',
|
||||
yellow: '\x1b[33m',
|
||||
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}`);
|
||||
console.log(`${colors.blue} Natural Language Agreement - Environment Switch${colors.reset}`);
|
||||
console.log(`${colors.blue}════════════════════════════════════════════════════════${colors.reset}\n`);
|
||||
|
||||
// If no environment specified, show current
|
||||
if (!env) {
|
||||
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(`${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'];
|
||||
if (!validEnvs.includes(env)) {
|
||||
console.error(`${colors.red}❌ Invalid environment: ${env}${colors.reset}`);
|
||||
console.log('Valid environments: devnet, sepolia, mainnet\n');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Switch environment
|
||||
const currentEnv = getCurrentEnvironment();
|
||||
|
||||
if (currentEnv === env) {
|
||||
console.log(`${colors.yellow}⚠️ Already on ${env}${colors.reset}\n`);
|
||||
return;
|
||||
}
|
||||
|
||||
setCurrentEnvironment(env);
|
||||
console.log(`${colors.green}✅ Switched to ${env}${colors.reset}\n`);
|
||||
|
||||
// Show info about the environment
|
||||
if (env === 'devnet') {
|
||||
console.log('📝 Using local Anvil blockchain (http://localhost:8545)');
|
||||
console.log(' Run "nla dev" to start the development environment\n');
|
||||
} 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 === 'mainnet') {
|
||||
console.log('📝 Using Ethereum mainnet');
|
||||
console.log(` ${colors.yellow}⚠️ WARNING: This is production! Use with caution.${colors.reset}\n`);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"network": "sepolia",
|
||||
"chainId": 11155111,
|
||||
"rpcUrl": "https://eth-sepolia.g.alchemy.com/v2/YOUR_ALCHEMY_KEY",
|
||||
"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": ""
|
||||
}
|
||||
}
|
||||
79
cli/index.ts
79
cli/index.ts
|
|
@ -1,11 +1,21 @@
|
|||
#!/usr/bin/env bun
|
||||
#!/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(`
|
||||
|
|
@ -19,6 +29,8 @@ 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)
|
||||
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
|
||||
|
|
@ -39,6 +51,7 @@ Options (vary by command):
|
|||
--arbitration-provider <name> Arbitration provider (create, default: OpenAI)
|
||||
--arbitration-model <model> Arbitration model (create, default: gpt-4o-mini)
|
||||
--arbitration-prompt <text> Custom arbitration prompt (create, optional)
|
||||
--env <file> Path to .env file (dev, default: .env)
|
||||
|
||||
Environment Variables:
|
||||
PRIVATE_KEY Private key for transactions
|
||||
|
|
@ -49,6 +62,9 @@ Examples:
|
|||
# Start development environment
|
||||
nla dev
|
||||
|
||||
# Start development with custom .env file
|
||||
nla dev --env /path/to/.env.production
|
||||
|
||||
# Deploy contracts
|
||||
nla deploy
|
||||
|
||||
|
|
@ -83,7 +99,7 @@ Examples:
|
|||
|
||||
// Parse command line arguments
|
||||
function parseCliArgs() {
|
||||
const args = Bun.argv.slice(2);
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length === 0) {
|
||||
displayHelp();
|
||||
|
|
@ -113,34 +129,19 @@ function parseCliArgs() {
|
|||
"arbitration-provider": { type: "string" },
|
||||
"arbitration-model": { type: "string" },
|
||||
"arbitration-prompt": { type: "string" },
|
||||
"env": { type: "string" },
|
||||
"environment": { type: "string" },
|
||||
},
|
||||
strict: true,
|
||||
strict: command !== "switch" && command !== "network", // Allow positional args for switch command
|
||||
allowPositionals: command === "switch" || command === "network",
|
||||
});
|
||||
|
||||
return { command, ...values };
|
||||
}
|
||||
|
||||
// Shell command handler
|
||||
async function runShellCommand(scriptName: string, args: string[] = []) {
|
||||
const { spawnSync } = await import("child_process");
|
||||
const scriptDir = import.meta.dir;
|
||||
const scriptPath = `${scriptDir}/scripts/${scriptName}`;
|
||||
|
||||
// Run the shell script
|
||||
const result = spawnSync(scriptPath, args, {
|
||||
stdio: "inherit",
|
||||
cwd: process.cwd(),
|
||||
shell: true,
|
||||
});
|
||||
|
||||
process.exit(result.status || 0);
|
||||
}
|
||||
|
||||
// Server command handler (for deploy.ts, oracle.ts)
|
||||
async function runServerCommand(scriptName: string, args: string[] = []) {
|
||||
const { spawnSync } = await import("child_process");
|
||||
const scriptDir = import.meta.dir;
|
||||
const scriptPath = `${scriptDir}/server/${scriptName}`;
|
||||
const scriptPath = join(__dirname, "server", scriptName);
|
||||
|
||||
// Run the TypeScript file directly
|
||||
const result = spawnSync("bun", ["run", scriptPath, ...args], {
|
||||
|
|
@ -157,25 +158,38 @@ async function main() {
|
|||
const args = parseCliArgs();
|
||||
const command = args.command;
|
||||
|
||||
// Handle shell script commands (dev and stop need shell for process management)
|
||||
// Handle dev and stop commands
|
||||
if (command === "dev") {
|
||||
await runShellCommand("dev.sh");
|
||||
await runDevCommand(__dirname, args.env as string | undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
if (command === "stop") {
|
||||
await runShellCommand("stop.sh");
|
||||
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.ts", Bun.argv.slice(3));
|
||||
await runServerCommand("deploy.js", process.argv.slice(3));
|
||||
return;
|
||||
}
|
||||
|
||||
if (command === "start-oracle") {
|
||||
await runServerCommand("oracle.ts", Bun.argv.slice(3));
|
||||
await runServerCommand("oracle.js", process.argv.slice(3));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -184,13 +198,13 @@ async function main() {
|
|||
|
||||
switch (command) {
|
||||
case "escrow:create":
|
||||
scriptPath = "./client/create-escrow.ts";
|
||||
scriptPath = "./client/create-escrow.js";
|
||||
break;
|
||||
case "escrow:fulfill":
|
||||
scriptPath = "./client/fulfill-escrow.ts";
|
||||
scriptPath = "./client/fulfill-escrow.js";
|
||||
break;
|
||||
case "escrow:collect":
|
||||
scriptPath = "./client/collect-escrow.ts";
|
||||
scriptPath = "./client/collect-escrow.js";
|
||||
break;
|
||||
case "escrow:status":
|
||||
await runStatusCommand(args);
|
||||
|
|
@ -203,11 +217,10 @@ async function main() {
|
|||
|
||||
// Run the command as a subprocess with the args (excluding the command name)
|
||||
const { spawnSync } = await import("child_process");
|
||||
const scriptDir = import.meta.dir;
|
||||
const fullScriptPath = `${scriptDir}/${scriptPath}`;
|
||||
const fullScriptPath = join(__dirname, scriptPath);
|
||||
|
||||
// Build args array without the command name
|
||||
const commandArgs = Bun.argv.slice(3); // Skip bun, script, and command
|
||||
const commandArgs = process.argv.slice(3); // Skip node, script, and command
|
||||
|
||||
const result = spawnSync("bun", ["run", fullScriptPath, ...commandArgs], {
|
||||
stdio: "inherit",
|
||||
|
|
|
|||
|
|
@ -1,138 +0,0 @@
|
|||
#!/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 LLM API keys
|
||||
if [ -z "$OPENAI_API_KEY" ] && [ -z "$ANTHROPIC_API_KEY" ] && [ -z "$OPENROUTER_API_KEY" ]; then
|
||||
echo -e "${RED}❌ No LLM provider API key set${NC}"
|
||||
echo "Please add at least one API key to your .env file:"
|
||||
echo " OPENAI_API_KEY=sk-..."
|
||||
echo " ANTHROPIC_API_KEY=sk-ant-..."
|
||||
echo " OPENROUTER_API_KEY=sk-or-..."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -n "$OPENAI_API_KEY" ]; then
|
||||
echo -e "${GREEN}✅ OpenAI API key configured${NC}"
|
||||
fi
|
||||
if [ -n "$ANTHROPIC_API_KEY" ]; then
|
||||
echo -e "${GREEN}✅ Anthropic API key configured${NC}"
|
||||
fi
|
||||
if [ -n "$OPENROUTER_API_KEY" ]; then
|
||||
echo -e "${GREEN}✅ OpenRouter API key configured${NC}"
|
||||
fi
|
||||
if [ -n "$PERPLEXITY_API_KEY" ]; then
|
||||
echo -e "${GREEN}✅ Perplexity API key configured${NC}"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# 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"
|
||||
bun run cli/server/deploy.ts --network localhost --rpc-url http://localhost:8545
|
||||
|
||||
# Start oracle
|
||||
echo -e "\n${BLUE}🚀 Starting oracle...${NC}\n"
|
||||
bun run cli/server/oracle.ts --deployment ./cli/deployments/localhost.json
|
||||
|
||||
# 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
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
#!/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}"
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
#!/usr/bin/env bun
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Deployment script for Alkahest Natural Language Agreement Oracle
|
||||
*
|
||||
|
|
@ -54,7 +54,7 @@ Examples:
|
|||
// Parse command line arguments
|
||||
function parseCliArgs() {
|
||||
const { values } = parseArgs({
|
||||
args: Bun.argv.slice(2),
|
||||
args: process.argv.slice(2),
|
||||
options: {
|
||||
"network": { type: "string" },
|
||||
"rpc-url": { type: "string" },
|
||||
|
|
@ -329,7 +329,10 @@ async function main() {
|
|||
// Get the script directory and go up to project root, then into cli/deployments
|
||||
const scriptDir = import.meta.dir;
|
||||
const projectRoot = resolve(scriptDir, "../..");
|
||||
const outputPath = args.output || resolve(projectRoot, `cli/deployments/${network}.json`);
|
||||
|
||||
// Map localhost to devnet for file naming
|
||||
const deploymentFileName = network === "localhost" ? "devnet" : network;
|
||||
const outputPath = args.output || resolve(projectRoot, `cli/deployments/${deploymentFileName}.json`);
|
||||
const outputDir = resolve(outputPath, "..");
|
||||
|
||||
if (!existsSync(outputDir)) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
#!/usr/bin/env bun
|
||||
#!/usr/bin/env node
|
||||
import { parseArgs } from "util";
|
||||
import { parseAbiParameters, createWalletClient, http, publicActions } from "viem";
|
||||
import { privateKeyToAccount } from "viem/accounts";
|
||||
|
|
@ -44,7 +44,7 @@ Examples:
|
|||
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-...
|
||||
bun oracle.ts --deployment ./deployments/devnet.json --private-key 0x... --openai-api-key sk-...
|
||||
|
||||
# Using environment variables
|
||||
export OPENAI_API_KEY=sk-...
|
||||
|
|
@ -57,7 +57,7 @@ Examples:
|
|||
// Parse command line arguments
|
||||
function parseCliArgs() {
|
||||
const { values } = parseArgs({
|
||||
args: Bun.argv.slice(2),
|
||||
args: process.argv.slice(2),
|
||||
options: {
|
||||
"rpc-url": { type: "string" },
|
||||
"private-key": { type: "string" },
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
/**
|
||||
* Shared utilities for NLA CLI
|
||||
*/
|
||||
|
||||
import { foundry, sepolia, mainnet } from "viem/chains";
|
||||
import type { Chain } from "viem/chains";
|
||||
|
||||
/**
|
||||
* Get viem chain configuration from network name
|
||||
*/
|
||||
export function getChainFromNetwork(network: string): Chain {
|
||||
switch (network.toLowerCase()) {
|
||||
case "localhost":
|
||||
case "devnet":
|
||||
return foundry;
|
||||
case "sepolia":
|
||||
return sepolia;
|
||||
case "mainnet":
|
||||
return mainnet;
|
||||
default:
|
||||
return foundry;
|
||||
}
|
||||
}
|
||||
46
package.json
46
package.json
|
|
@ -1,16 +1,50 @@
|
|||
{
|
||||
"name": "natural-language-agreement-extension",
|
||||
"module": "index.ts",
|
||||
"name": "nla",
|
||||
"version": "1.0.3",
|
||||
"description": "Natural Language Agreement Oracle - CLI for creating and managing blockchain agreements using natural language",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest"
|
||||
"private": false,
|
||||
"author": "Arkhai",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/arkhai-io/natural-language-agreements.git"
|
||||
},
|
||||
"keywords": [
|
||||
"blockchain",
|
||||
"ethereum",
|
||||
"smart-contracts",
|
||||
"natural-language",
|
||||
"oracle",
|
||||
"ai",
|
||||
"llm",
|
||||
"escrow"
|
||||
],
|
||||
"bin": {
|
||||
"nla": "cli/index.ts"
|
||||
"nla": "dist/cli/index.js"
|
||||
},
|
||||
"files": [
|
||||
"dist/**/*",
|
||||
"README.md",
|
||||
".env.example"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"preferGlobal": true,
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
"@types/node": "^20.0.0",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc --project tsconfig.build.json && chmod +x dist/cli/index.js",
|
||||
"prepublishOnly": "npm run build",
|
||||
"patch": "npm version patch",
|
||||
"minor": "npm version minor",
|
||||
"major": "npm version major",
|
||||
"dev": "bun run index.ts",
|
||||
"public": "npm publish --access public",
|
||||
"start": "bun run index.ts",
|
||||
"test": "bun test ./tests --exclude alkahest-ts/** ",
|
||||
"setup": "bun run cli/index.ts dev",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./",
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["cli/**/*"],
|
||||
"exclude": ["node_modules", "tests", "dist"]
|
||||
}
|
||||
|
|
@ -1,30 +1,16 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
// Environment setup & latest features
|
||||
"lib": ["ESNext"],
|
||||
"target": "ESNext",
|
||||
"module": "Preserve",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": ".",
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
// Best practices
|
||||
"esModuleInterop": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false,
|
||||
|
||||
}
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": [
|
||||
"cli/**/*"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue