natural-language-agreements/cli/client/create-escrow.ts

307 lines
11 KiB
TypeScript

#!/usr/bin/env bun
/**
* CLI tool to create a Natural Language Agreement escrow
*
* This allows users to create an escrow with a natural language demand
* that will be arbitrated by the oracle.
*/
import { parseArgs } from "util";
import { createWalletClient, http, publicActions, parseEther } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { existsSync, readFileSync } from "fs";
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]
Options:
--demand <text> Natural language demand (required)
--amount <number> Amount of tokens to escrow (required)
--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: 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)
--arbitration-prompt <text> Custom arbitration prompt (optional)
--help, -h Display this help message
Environment Variables (alternative to CLI options):
PRIVATE_KEY Your private key
RPC_URL Custom RPC URL
Examples:
# Create an escrow for a simple demand
bun cli/create-escrow.ts \\
--demand "The sky is blue" \\
--amount 10 \\
--token 0x5FbDB2315678afecb367f032d93F642f64180aa3 \\
--oracle 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 \\
--private-key 0x...
# Using environment variables
export PRIVATE_KEY=0x...
bun cli/create-escrow.ts --demand "Deliver package by Friday" --amount 100 --token 0x... --oracle 0x...
`);
}
// Parse command line arguments
function parseCliArgs() {
const { values } = parseArgs({
args: Bun.argv.slice(2),
options: {
"demand": { type: "string" },
"amount": { type: "string" },
"token": { type: "string" },
"oracle": { type: "string" },
"private-key": { type: "string" },
"deployment": { type: "string" },
"rpc-url": { type: "string" },
"arbitration-provider": { type: "string" },
"arbitration-model": { type: "string" },
"arbitration-prompt": { type: "string" },
"help": { type: "boolean", short: "h" },
},
strict: true,
});
return values;
}
async function main() {
try {
const args = parseCliArgs();
// Display help if requested
if (args.help) {
displayHelp();
process.exit(0);
}
// Get configuration
const demand = args.demand;
const amount = args.amount;
const tokenAddress = args.token;
const oracleAddress = args.oracle;
const privateKey = args["private-key"] || process.env.PRIVATE_KEY;
const deploymentPath = args.deployment || `./cli/deployments/${getCurrentEnvironment()}.json`;
// Arbitration configuration with defaults
const arbitrationProvider = args["arbitration-provider"] || "OpenAI";
const arbitrationModel = args["arbitration-model"] || "gpt-4o-mini";
const arbitrationPrompt = args["arbitration-prompt"] ||
`Evaluate the fulfillment against the demand and decide whether the demand was validly fulfilled
Demand: {{demand}}
Fulfillment: {{obligation}}`;
// Validate required parameters
if (!demand) {
console.error("❌ Error: Demand is required. Use --demand <text>");
console.error("Run with --help for usage information.");
process.exit(1);
}
if (!amount) {
console.error("❌ Error: Amount is required. Use --amount <number>");
console.error("Run with --help for usage information.");
process.exit(1);
}
if (!tokenAddress) {
console.error("❌ Error: Token address is required. Use --token <address>");
console.error("Run with --help for usage information.");
process.exit(1);
}
if (!oracleAddress) {
console.error("❌ Error: Oracle address is required. Use --oracle <address>");
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 PRIVATE_KEY");
console.error("Run with --help for usage information.");
process.exit(1);
}
// Load deployment file
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(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:");
console.log(` 📝 Demand: "${demand}"`);
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,
transport: http(rpcUrl),
}).extend(publicActions);
console.log(`✅ User address: ${account.address}\n`);
// Check balance
const balance = await walletClient.getBalance({ address: account.address });
console.log(`💰 ETH balance: ${parseFloat((balance / 10n ** 18n).toString()).toFixed(4)} ETH\n`);
if (balance === 0n) {
console.error("❌ Error: Account has no ETH for gas. Please fund the account first.");
process.exit(1);
}
// Create alkahest client
const client = makeClient(
walletClient as any,
deployment.addresses
);
// Extend with LLM client (only for encoding the demand, no API calls needed)
const llmClient = client.extend((c) => ({
llm: makeLLMClient([]),
}));
// Check token balance
const tokenBalance = await walletClient.readContract({
address: tokenAddress as `0x${string}`,
abi: fixtures.MockERC20Permit.abi,
functionName: "balanceOf",
args: [account.address],
}) as bigint;
console.log(`💰 Token balance: ${tokenBalance.toString()} tokens\n`);
if (tokenBalance < BigInt(amount)) {
console.error(`❌ Error: Insufficient token balance. You have ${tokenBalance.toString()} but need ${amount}`);
process.exit(1);
}
console.log("📋 Creating escrow\n");
// Encode the demand with oracle arbiter
const arbiter = deployment.addresses.trustedOracleArbiter;
const encodedDemand = client.arbiters.general.trustedOracle.encodeDemand({
oracle: oracleAddress as `0x${string}`,
data: llmClient.llm.encodeDemand({
arbitrationProvider,
arbitrationModel,
arbitrationPrompt,
demand: demand
})
});
// Create the escrow
const { attested: escrow } = await client.erc20.escrow.nonTierable.permitAndCreate(
{
address: tokenAddress as `0x${string}`,
value: BigInt(amount),
},
{ arbiter, demand: encodedDemand },
0n,
);
console.log("✨ Escrow created successfully!\n");
console.log("📋 Escrow Details:");
console.log(` UID: ${escrow.uid}`);
console.log(` Attester: ${escrow.attester}`);
console.log(` Recipient: ${escrow.recipient}`);
console.log("🎯 Next Steps:");
console.log("1. Someone fulfills the obligation:");
console.log(` nla escrow:fulfill \\`);
console.log(` --escrow-uid ${escrow.uid} \\`);
console.log(` --fulfillment "Yes, the sky is blue" \\`);
console.log(` --oracle ${oracleAddress}`);
console.log("\n2. The oracle will arbitrate the fulfillment automatically");
console.log("\n3. If approved, collect the escrow:");
console.log(` nla escrow:collect \\`);
console.log(` --escrow-uid ${escrow.uid} \\`);
console.log(` --fulfillment-uid <fulfillment-uid>`);
} catch (error) {
console.error("❌ Failed to create escrow:", error);
process.exit(1);
}
}
// Run the CLI
main();