#!/usr/bin/env bun /** * CLI tool to fulfill a Natural Language Agreement escrow * * This allows users to fulfill an existing escrow by providing * the fulfillment text that will be arbitrated by the oracle. */ import { parseArgs } from "util"; import { createWalletClient, http, publicActions, formatEther, toHex, keccak256 } 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 { getChainFromNetwork, loadDeploymentWithDefaults, getPrivateKey } from "../utils.js"; // Get the directory of the current module const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); // Helper function to display usage function displayHelp() { console.log(` Natural Language Agreement Fulfillment CLI Fulfill an existing escrow with your completion text. Usage: bun cli/fulfill-escrow.ts [options] Options: --escrow-uid Escrow UID to fulfill (required) --fulfillment Your fulfillment text (required) --oracle
Oracle address that will arbitrate (required) --private-key Your private key (required) --deployment Path to deployment file (default: ./cli/deployments/anvil.json) --rpc-url RPC URL (default: from deployment file) --help, -h Display this help message Environment Variables (alternative to CLI options): PRIVATE_KEY Your private key RPC_URL Custom RPC URL Examples: # Fulfill an escrow bun cli/fulfill-escrow.ts \\ --escrow-uid 0x... \\ --fulfillment "The sky appears blue today" \\ --oracle 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 \\ --private-key 0x... # Using environment variables export PRIVATE_KEY=0x... bun cli/fulfill-escrow.ts --escrow-uid 0x... --fulfillment "Package delivered" --oracle 0x... `); } // Parse command line arguments function parseCliArgs() { const { values } = parseArgs({ args: Bun.argv.slice(2), options: { "escrow-uid": { type: "string" }, "fulfillment": { type: "string" }, "oracle": { type: "string" }, "private-key": { type: "string" }, "deployment": { type: "string" }, "rpc-url": { 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 escrowUid = args["escrow-uid"]; const fulfillment = args.fulfillment; const oracleAddress = args.oracle; const privateKey = args["private-key"] || getPrivateKey(); const deploymentPath = args.deployment ; // Validate required parameters if (!escrowUid) { console.error("āŒ Error: Escrow UID is required. Use --escrow-uid "); console.error("Run with --help for usage information."); process.exit(1); } if (!fulfillment) { console.error("āŒ Error: Fulfillment text is required. Use --fulfillment "); console.error("Run with --help for usage information."); process.exit(1); } if (!oracleAddress) { console.error("āŒ Error: Oracle address is required. Use --oracle
"); console.error("Run with --help for usage information."); process.exit(1); } if (!privateKey) { console.error("āŒ Error: Private key is required"); console.error("\nšŸ’” You can either:"); console.error(" 1. Set it globally: nla wallet:set --private-key "); console.error(" 2. Use for this command only: --private-key "); console.error(" 3. Set PRIVATE_KEY environment variable"); console.error("\nRun with --help for usage information."); process.exit(1); } // Load deployment file (auto-detects current network if not specified) let deployment; try { deployment = loadDeploymentWithDefaults(deploymentPath); } catch (error) { console.error(`āŒ Error: ${(error as Error).message}`); console.error("Please deploy contracts first or specify correct path with --deployment"); process.exit(1); } 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, transport: http(rpcUrl), }).extend(publicActions); console.log(`āœ… Fulfiller address: ${account.address}\n`); // Check balance const balance = await walletClient.getBalance({ address: account.address }); console.log(`šŸ’° ETH balance: ${parseFloat(formatEther(balance)).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 ); console.log("šŸ“‹ Creating fulfillment obligation...\n"); // Create the fulfillment using CommitRevealObligation (commit-reveal flow) const schema = keccak256(toHex("{item:string}")); const salt = keccak256(toHex(crypto.randomUUID())); const payload = toHex(fulfillment); const obligationData = { payload, salt, schema }; // Step 1: Compute and submit commitment console.log("šŸ”’ Computing commitment..."); const commitment = await client.commitReveal.computeCommitment( escrowUid as `0x${string}`, account.address, obligationData, ); console.log(` Commitment: ${commitment}`); console.log("šŸ“ Submitting commitment (with bond)..."); const { hash: commitHash } = await client.commitReveal.commit(commitment); console.log(` Commit tx: ${commitHash}`); // Step 2: Wait for next block console.log("ā³ Waiting for next block..."); await walletClient.waitForTransactionReceipt({ hash: commitHash }); // Step 3: Reveal - create the obligation console.log("šŸ”“ Revealing obligation..."); const { attested: fulfillmentAttestation } = await client.commitReveal.doObligation( obligationData, escrowUid as `0x${string}`, ); // Step 4: Reclaim bond console.log("šŸ’° Reclaiming bond..."); await client.commitReveal.reclaimBond(fulfillmentAttestation.uid); console.log("āœ… Fulfillment created!\n"); console.log("šŸ“‹ Fulfillment Details:"); console.log(` UID: ${fulfillmentAttestation.uid}`); console.log(` Attester: ${fulfillmentAttestation.attester}\n`); console.log("šŸ“¤ Requesting arbitration from oracle...\n"); const escrow = await client.getAttestation(escrowUid as `0x${string}`); const decodedEscrow = client.erc20.escrow.nonTierable.decodeObligation(escrow.data); // Request arbitration await client.arbiters.general.trustedOracle.requestArbitration( fulfillmentAttestation.uid, oracleAddress as `0x${string}`, decodedEscrow.demand ); console.log("✨ Arbitration requested successfully!\n"); console.log("šŸŽÆ Next Steps:"); console.log("1. Wait for the oracle to arbitrate (usually a few seconds)"); console.log("\n2. If approved, collect the escrow:"); console.log(` nla escrow:collect \\`); console.log(` --escrow-uid ${escrowUid} \\`); console.log(` --fulfillment-uid ${fulfillmentAttestation.uid}`); } catch (error) { console.error("āŒ Failed to fulfill escrow:", error); process.exit(1); } } // Run the CLI main();