feat: add escrow:arbitrate CLI command for manual arbitration
Add nla escrow:arbitrate with support for a specific escrow UID or "all" to scan for unarbitrated fulfillments demanding the user as oracle. Supports interactive mode (approve/reject/skip) and --auto mode using the LLM specified in the escrow's demand. Update nla-arbitrate skill to reference the new CLI command instead of raw SDK scripts. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c01364a594
commit
ba498be935
|
|
@ -132,6 +132,7 @@ nla stop
|
|||
| `nla escrow:fulfill [options]` | Submit fulfillment for an escrow |
|
||||
| `nla escrow:collect [options]` | Collect approved escrow funds |
|
||||
| `nla escrow:status --escrow-uid <uid>` | Check escrow status |
|
||||
| `nla escrow:arbitrate [options]` | Manually arbitrate escrow fulfillments |
|
||||
|
||||
### Environment Management
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,381 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* CLI tool to manually arbitrate NLA escrow fulfillments
|
||||
*
|
||||
* This allows an oracle operator to manually review and submit
|
||||
* arbitration decisions for escrow fulfillments, as an alternative
|
||||
* to the automated oracle listener.
|
||||
*/
|
||||
|
||||
import { parseArgs } from "util";
|
||||
import { createWalletClient, createPublicClient, http, publicActions, fromHex } from "viem";
|
||||
import { privateKeyToAccount } from "viem/accounts";
|
||||
import { makeClient } from "alkahest-ts";
|
||||
import { contracts } from "alkahest-ts";
|
||||
import { makeLLMClient } from "../..";
|
||||
import { ProviderName } from "../../nla";
|
||||
import {
|
||||
getCurrentEnvironment,
|
||||
getChainFromNetwork,
|
||||
loadDeploymentWithDefaults,
|
||||
getPrivateKey,
|
||||
loadEnvFile,
|
||||
} from "../utils.js";
|
||||
import { existsSync } from "fs";
|
||||
import { resolve } from "path";
|
||||
import { createInterface } from "readline";
|
||||
|
||||
// Helper function to display usage
|
||||
function displayHelp() {
|
||||
console.log(`
|
||||
Natural Language Agreement - Arbitrate Escrow Fulfillments
|
||||
|
||||
Manually review and submit arbitration decisions for escrow fulfillments.
|
||||
Your wallet address must match the oracle specified in the escrow.
|
||||
|
||||
Usage:
|
||||
nla escrow:arbitrate [options]
|
||||
|
||||
Options:
|
||||
--escrow-uid <uid|all> Escrow UID to arbitrate, or "all" to scan for pending requests (required)
|
||||
--private-key <key> Oracle operator's private key
|
||||
--deployment <file> Load addresses from deployment file (optional)
|
||||
--rpc-url <url> RPC URL (optional, from deployment file)
|
||||
--auto Auto-arbitrate using LLM (skip interactive confirmation)
|
||||
--openai-api-key <key> OpenAI API key (for auto mode)
|
||||
--anthropic-api-key <key> Anthropic API key (for auto mode)
|
||||
--openrouter-api-key <key> OpenRouter API key (for auto mode)
|
||||
--perplexity-api-key <key> Perplexity API key (for auto mode)
|
||||
--env <file> Path to .env file (default: .env)
|
||||
--help, -h Display this help message
|
||||
|
||||
Examples:
|
||||
# Arbitrate a specific escrow (interactive)
|
||||
nla escrow:arbitrate --escrow-uid 0x...
|
||||
|
||||
# Scan for all pending arbitration requests
|
||||
nla escrow:arbitrate --escrow-uid all
|
||||
|
||||
# Auto-arbitrate using LLM (non-interactive)
|
||||
nla escrow:arbitrate --escrow-uid 0x... --auto
|
||||
`);
|
||||
}
|
||||
|
||||
// Parse command line arguments
|
||||
function parseCliArgs() {
|
||||
const { values } = parseArgs({
|
||||
args: process.argv.slice(2),
|
||||
options: {
|
||||
"escrow-uid": { type: "string" },
|
||||
"private-key": { type: "string" },
|
||||
"deployment": { type: "string" },
|
||||
"rpc-url": { type: "string" },
|
||||
"auto": { type: "boolean" },
|
||||
"openai-api-key": { type: "string" },
|
||||
"anthropic-api-key": { type: "string" },
|
||||
"openrouter-api-key": { type: "string" },
|
||||
"perplexity-api-key": { type: "string" },
|
||||
"env": { type: "string" },
|
||||
"help": { type: "boolean", short: "h" },
|
||||
},
|
||||
strict: true,
|
||||
});
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
function prompt(question: string): Promise<string> {
|
||||
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
||||
return new Promise((resolve) => {
|
||||
rl.question(question, (answer) => {
|
||||
rl.close();
|
||||
resolve(answer.trim());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
type PendingArbitration = {
|
||||
escrowUid: `0x${string}`;
|
||||
fulfillmentUid: `0x${string}`;
|
||||
arbitrationRequestUid: `0x${string}`;
|
||||
demand: `0x${string}`;
|
||||
demandText: string;
|
||||
fulfillmentText: string;
|
||||
nlaDemand: {
|
||||
arbitrationProvider: string;
|
||||
arbitrationModel: string;
|
||||
arbitrationPrompt: string;
|
||||
demand: string;
|
||||
};
|
||||
};
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
const args = parseCliArgs();
|
||||
|
||||
if (args.help) {
|
||||
displayHelp();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Load .env file
|
||||
const envPath = args.env || ".env";
|
||||
const resolvedEnvPath = resolve(process.cwd(), envPath);
|
||||
if (existsSync(resolvedEnvPath)) {
|
||||
loadEnvFile(resolvedEnvPath);
|
||||
}
|
||||
|
||||
const escrowUidArg = args["escrow-uid"];
|
||||
const privateKey = args["private-key"] || getPrivateKey();
|
||||
const deploymentFile = args["deployment"];
|
||||
const autoMode = args["auto"] || false;
|
||||
|
||||
if (!escrowUidArg) {
|
||||
console.error("❌ Error: --escrow-uid is required (use a UID or \"all\")");
|
||||
console.error("Run with --help for usage information.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!privateKey) {
|
||||
console.error("❌ Error: Private key is required");
|
||||
console.error("\n💡 You can either:");
|
||||
console.error(" 1. Set it globally: nla wallet:set --private-key <your-key>");
|
||||
console.error(" 2. Use for this command only: --private-key <your-key>");
|
||||
console.error(" 3. Set PRIVATE_KEY environment variable");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Load deployment
|
||||
const deployment = loadDeploymentWithDefaults(deploymentFile);
|
||||
const rpcUrl = args["rpc-url"] || deployment.rpcUrl;
|
||||
const chain = getChainFromNetwork(deployment.network);
|
||||
const addresses = deployment.addresses;
|
||||
|
||||
// Create clients
|
||||
const account = privateKeyToAccount(privateKey as `0x${string}`);
|
||||
const walletClient = createWalletClient({
|
||||
account,
|
||||
chain,
|
||||
transport: http(rpcUrl),
|
||||
}).extend(publicActions) as any;
|
||||
|
||||
const publicClient = createPublicClient({
|
||||
chain,
|
||||
transport: http(rpcUrl),
|
||||
});
|
||||
|
||||
const client = makeClient(walletClient, addresses);
|
||||
const llmClient = makeLLMClient([]);
|
||||
|
||||
console.log(`⚖️ NLA Manual Arbitration\n`);
|
||||
console.log(` Oracle address: ${account.address}`);
|
||||
console.log(` Network: ${deployment.network}`);
|
||||
console.log(` RPC URL: ${rpcUrl}\n`);
|
||||
|
||||
// Set up LLM providers for auto mode
|
||||
if (autoMode) {
|
||||
const openaiApiKey = args["openai-api-key"] || process.env.OPENAI_API_KEY;
|
||||
const anthropicApiKey = args["anthropic-api-key"] || process.env.ANTHROPIC_API_KEY;
|
||||
const openrouterApiKey = args["openrouter-api-key"] || process.env.OPENROUTER_API_KEY;
|
||||
const perplexityApiKey = args["perplexity-api-key"] || process.env.PERPLEXITY_API_KEY;
|
||||
|
||||
if (!openaiApiKey && !anthropicApiKey && !openrouterApiKey) {
|
||||
console.error("❌ Error: Auto mode requires at least one LLM provider API key.");
|
||||
console.error(" Set OPENAI_API_KEY, ANTHROPIC_API_KEY, or OPENROUTER_API_KEY");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (openaiApiKey) {
|
||||
llmClient.addProvider({ providerName: ProviderName.OpenAI, apiKey: openaiApiKey, perplexityApiKey });
|
||||
}
|
||||
if (anthropicApiKey) {
|
||||
llmClient.addProvider({ providerName: ProviderName.Anthropic, apiKey: anthropicApiKey, perplexityApiKey });
|
||||
}
|
||||
if (openrouterApiKey) {
|
||||
llmClient.addProvider({ providerName: ProviderName.OpenRouter, apiKey: openrouterApiKey, perplexityApiKey });
|
||||
}
|
||||
}
|
||||
|
||||
// Find pending arbitration requests
|
||||
const pending: PendingArbitration[] = [];
|
||||
|
||||
console.log("🔍 Scanning for arbitration requests...\n");
|
||||
|
||||
// Get all Attested events from EAS
|
||||
const filter = await publicClient.createContractEventFilter({
|
||||
address: addresses.eas as `0x${string}`,
|
||||
abi: contracts.IEAS.abi.abi,
|
||||
eventName: "Attested",
|
||||
fromBlock: 0n,
|
||||
});
|
||||
const events = await publicClient.getFilterLogs({ filter });
|
||||
|
||||
// Determine which escrows to check
|
||||
const scanAll = escrowUidArg.toLowerCase() === "all";
|
||||
const targetEscrowUid = scanAll ? null : escrowUidArg as `0x${string}`;
|
||||
|
||||
// Build a map of all attestation UIDs for quick lookup
|
||||
const attestationUids = new Set(events.map((e: any) => e.args?.uid?.toLowerCase()));
|
||||
|
||||
// Find fulfillments (attestations that reference an escrow)
|
||||
for (const event of events) {
|
||||
const fulfillmentUid = (event as any).args?.uid as `0x${string}`;
|
||||
const refUid = (event as any).args?.refUID as `0x${string}`;
|
||||
if (!fulfillmentUid || !refUid) continue;
|
||||
|
||||
// Skip if we're targeting a specific escrow and this doesn't match
|
||||
if (!scanAll && refUid.toLowerCase() !== targetEscrowUid!.toLowerCase()) continue;
|
||||
|
||||
// Check if refUid points to a valid escrow
|
||||
// A fulfillment references an escrow, and an arbitration decision references a fulfillment
|
||||
// We need to check if this is a fulfillment (not an arbitration decision)
|
||||
let escrowAttestation: any;
|
||||
try {
|
||||
escrowAttestation = await publicClient.readContract({
|
||||
address: addresses.eas as `0x${string}`,
|
||||
abi: contracts.IEAS.abi.abi,
|
||||
functionName: "getAttestation",
|
||||
args: [refUid],
|
||||
});
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try to decode as an escrow obligation to verify it's actually an escrow
|
||||
let escrowData: any;
|
||||
try {
|
||||
escrowData = client.erc20.escrow.nonTierable.decodeObligation(escrowAttestation.data);
|
||||
} catch {
|
||||
continue; // Not an escrow
|
||||
}
|
||||
|
||||
// Decode the demand to check if our address is the oracle
|
||||
let trustedOracleDemand: any;
|
||||
let nlaDemand: any;
|
||||
try {
|
||||
trustedOracleDemand = client.arbiters.general.trustedOracle.decodeDemand(escrowData.demand);
|
||||
nlaDemand = llmClient.decodeDemand(trustedOracleDemand.data);
|
||||
} catch {
|
||||
continue; // Not an NLA demand or not using trusted oracle arbiter
|
||||
}
|
||||
|
||||
// Check if we are the oracle for this escrow
|
||||
if (trustedOracleDemand.oracle.toLowerCase() !== account.address.toLowerCase()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if this fulfillment already has an arbitration decision
|
||||
const hasDecision = events.some((e) => {
|
||||
const eRefUid = (e as any).args?.refUID;
|
||||
return eRefUid && eRefUid.toLowerCase() === fulfillmentUid.toLowerCase();
|
||||
});
|
||||
|
||||
if (hasDecision) continue;
|
||||
|
||||
// Decode the fulfillment text
|
||||
let fulfillmentText: string;
|
||||
try {
|
||||
const fulfillmentAttestation = await publicClient.readContract({
|
||||
address: addresses.eas as `0x${string}`,
|
||||
abi: contracts.IEAS.abi.abi,
|
||||
functionName: "getAttestation",
|
||||
args: [fulfillmentUid],
|
||||
});
|
||||
const commitRevealData = client.commitReveal.decode((fulfillmentAttestation as any).data);
|
||||
fulfillmentText = fromHex(commitRevealData.payload, "string");
|
||||
} catch {
|
||||
continue; // Can't decode fulfillment
|
||||
}
|
||||
|
||||
pending.push({
|
||||
escrowUid: refUid,
|
||||
fulfillmentUid,
|
||||
arbitrationRequestUid: fulfillmentUid,
|
||||
demand: escrowData.demand,
|
||||
demandText: nlaDemand.demand,
|
||||
fulfillmentText,
|
||||
nlaDemand,
|
||||
});
|
||||
}
|
||||
|
||||
if (pending.length === 0) {
|
||||
if (scanAll) {
|
||||
console.log("✅ No pending arbitration requests found for your oracle address.\n");
|
||||
} else {
|
||||
console.log(`✅ No pending arbitration requests found for escrow ${escrowUidArg}.\n`);
|
||||
console.log(" This could mean:");
|
||||
console.log(" - No fulfillments have been submitted yet");
|
||||
console.log(" - All fulfillments have already been arbitrated");
|
||||
console.log(" - Your address is not the oracle for this escrow");
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log(`📋 Found ${pending.length} pending arbitration request(s):\n`);
|
||||
|
||||
// Process each pending arbitration
|
||||
for (const item of pending) {
|
||||
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
||||
console.log(`📦 Escrow: ${item.escrowUid}`);
|
||||
console.log(`📨 Fulfillment: ${item.fulfillmentUid}`);
|
||||
console.log(`📝 Demand: "${item.demandText}"`);
|
||||
console.log(`💬 Fulfillment: "${item.fulfillmentText}"`);
|
||||
console.log(`🤖 Provider: ${item.nlaDemand.arbitrationProvider} / ${item.nlaDemand.arbitrationModel}`);
|
||||
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
|
||||
|
||||
let decision: boolean;
|
||||
|
||||
if (autoMode) {
|
||||
// Use LLM to arbitrate
|
||||
console.log(`🤔 Arbitrating with ${item.nlaDemand.arbitrationProvider}...`);
|
||||
decision = await llmClient.arbitrate(item.nlaDemand, item.fulfillmentText);
|
||||
console.log(` LLM decision: ${decision ? "✅ APPROVE" : "❌ REJECT"}\n`);
|
||||
} else {
|
||||
// Interactive mode - ask the user
|
||||
const answer = await prompt(" Enter decision (approve/reject): ");
|
||||
const normalized = answer.toLowerCase();
|
||||
if (normalized === "approve" || normalized === "a" || normalized === "yes" || normalized === "y" || normalized === "true") {
|
||||
decision = true;
|
||||
} else if (normalized === "reject" || normalized === "r" || normalized === "no" || normalized === "n" || normalized === "false") {
|
||||
decision = false;
|
||||
} else if (normalized === "skip" || normalized === "s") {
|
||||
console.log(" Skipped.\n");
|
||||
continue;
|
||||
} else {
|
||||
console.log(" Unrecognized input, skipping.\n");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Submit the decision on-chain
|
||||
console.log(`📤 Submitting ${decision ? "APPROVE" : "REJECT"} decision on-chain...`);
|
||||
try {
|
||||
const { unwatch } = await client.arbiters.general.trustedOracle.arbitrateMany(
|
||||
async () => decision,
|
||||
{
|
||||
onAfterArbitrate: async (result: any) => {
|
||||
console.log(` ✅ Decision recorded!`);
|
||||
console.log(` Decision UID: ${result.attestation.uid}`);
|
||||
console.log(` Result: ${result.decision ? "APPROVED" : "REJECTED"}\n`);
|
||||
},
|
||||
pollingInterval: 1000,
|
||||
}
|
||||
);
|
||||
|
||||
// Wait briefly for the arbitration to be picked up and processed
|
||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||
unwatch();
|
||||
} catch (error: any) {
|
||||
console.error(` ❌ Failed to submit decision: ${error.message}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("✨ Arbitration complete!\n");
|
||||
|
||||
} catch (error) {
|
||||
console.error("❌ Fatal error:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
15
cli/index.ts
15
cli/index.ts
|
|
@ -39,6 +39,7 @@ Commands:
|
|||
escrow:fulfill Fulfill an existing escrow
|
||||
escrow:collect Collect an approved escrow
|
||||
escrow:status Check the status of an escrow
|
||||
escrow:arbitrate Manually arbitrate escrow fulfillments
|
||||
help Display this help message
|
||||
|
||||
Options (vary by command):
|
||||
|
|
@ -98,6 +99,12 @@ Examples:
|
|||
|
||||
# Check escrow status
|
||||
nla escrow:status --escrow-uid 0x...
|
||||
|
||||
# Manually arbitrate a specific escrow
|
||||
nla escrow:arbitrate --escrow-uid 0x...
|
||||
|
||||
# Scan for all pending arbitration requests
|
||||
nla escrow:arbitrate --escrow-uid all
|
||||
`);
|
||||
}
|
||||
|
||||
|
|
@ -135,6 +142,11 @@ function parseCliArgs() {
|
|||
"arbitration-prompt": { type: "string" },
|
||||
"env": { type: "string" },
|
||||
"environment": { type: "string" },
|
||||
"auto": { type: "boolean" },
|
||||
"openai-api-key": { type: "string" },
|
||||
"anthropic-api-key": { type: "string" },
|
||||
"openrouter-api-key": { type: "string" },
|
||||
"perplexity-api-key": { type: "string" },
|
||||
"help": { type: "boolean", short: "h" },
|
||||
},
|
||||
strict: command !== "switch" && command !== "network", // Allow positional args for switch command
|
||||
|
|
@ -235,6 +247,9 @@ async function main() {
|
|||
case "escrow:status":
|
||||
scriptPath = "./client/status-escrow.js";
|
||||
break;
|
||||
case "escrow:arbitrate":
|
||||
scriptPath = "./client/arbitrate-escrow.js";
|
||||
break;
|
||||
default:
|
||||
console.error(`❌ Unknown command: ${command}`);
|
||||
console.error("Run 'nla help' for usage information.");
|
||||
|
|
|
|||
|
|
@ -1,167 +1,101 @@
|
|||
---
|
||||
name: nla-arbitrate
|
||||
description: Manually arbitrate NLA escrows as an alternative to the automated oracle. Use when the user wants to act as an AI arbiter themselves - fetching pending escrows that name the agent as oracle, evaluating demands against fulfillments, and submitting on-chain arbitration decisions via the alkahest TypeScript SDK.
|
||||
description: Manually arbitrate NLA escrow fulfillments as an alternative to the automated oracle. Use when the user wants to review pending arbitration requests, evaluate demands against fulfillments, and submit on-chain decisions. Supports both interactive and LLM-auto modes.
|
||||
metadata:
|
||||
author: arkhai
|
||||
version: "1.0"
|
||||
compatibility: Requires bun or node. Requires alkahest-ts and nla packages. Requires a funded Ethereum wallet whose address matches the oracle specified in escrows.
|
||||
allowed-tools: Bash Read Write
|
||||
compatibility: Requires nla CLI installed (npm install -g nla). Requires a funded Ethereum wallet whose address matches the oracle specified in escrows.
|
||||
allowed-tools: Bash(nla:*) Read
|
||||
---
|
||||
|
||||
# Manual NLA Arbitration
|
||||
|
||||
Act as an AI arbiter for NLA escrows, bypassing the automated oracle listener. Fetch escrow data, evaluate demands against fulfillments, and submit arbitration decisions on-chain.
|
||||
Manually arbitrate escrow fulfillments using the `nla escrow:arbitrate` CLI command, bypassing the automated oracle listener.
|
||||
|
||||
## When to use this
|
||||
|
||||
- The user wants to manually review and decide on escrow fulfillments instead of relying on the automated oracle
|
||||
- The user wants to manually review and decide on escrow fulfillments
|
||||
- The user is the oracle (their wallet address was specified as the oracle when escrows were created)
|
||||
- The automated oracle is not running, or the user wants more control over decisions
|
||||
|
||||
## Step-by-step instructions
|
||||
|
||||
### 1. Determine the oracle address
|
||||
### 1. Verify oracle identity
|
||||
|
||||
The user's wallet address must be the oracle specified in the escrow. Check:
|
||||
The user's wallet must be the oracle address specified in the escrow:
|
||||
|
||||
```bash
|
||||
nla wallet:show
|
||||
```
|
||||
|
||||
### 2. Fetch pending arbitration requests
|
||||
### 2a. Arbitrate a specific escrow
|
||||
|
||||
Write and run a TypeScript script to find escrows pending arbitration. The script uses the alkahest-ts SDK to query on-chain state.
|
||||
To review fulfillments for a known escrow UID:
|
||||
|
||||
```typescript
|
||||
import { createWalletClient, createPublicClient, http, publicActions, fromHex } from "viem";
|
||||
import { privateKeyToAccount } from "viem/accounts";
|
||||
import { makeClient } from "alkahest-ts";
|
||||
import { contracts } from "alkahest-ts";
|
||||
import { makeLLMClient } from "nla";
|
||||
```bash
|
||||
# Interactive mode - prompts for approve/reject
|
||||
nla escrow:arbitrate --escrow-uid <uid>
|
||||
|
||||
// Setup - adjust these for the user's environment
|
||||
const deployment = JSON.parse(require("fs").readFileSync("<deployment-file>", "utf-8"));
|
||||
// Deployment files are at: cli/deployments/anvil.json, sepolia.json, base-sepolia.json, mainnet.json
|
||||
// Or detect automatically using nla's utils
|
||||
|
||||
const chain = /* appropriate viem chain */;
|
||||
const account = privateKeyToAccount("<private-key>" as `0x${string}`);
|
||||
const walletClient = createWalletClient({
|
||||
account,
|
||||
chain,
|
||||
transport: http(deployment.rpcUrl),
|
||||
}).extend(publicActions);
|
||||
|
||||
const client = makeClient(walletClient as any, deployment.addresses);
|
||||
const llmClient = makeLLMClient([]);
|
||||
|
||||
const publicClient = createPublicClient({
|
||||
chain,
|
||||
transport: http(deployment.rpcUrl),
|
||||
});
|
||||
|
||||
// Query all Attested events from EAS
|
||||
const filter = await publicClient.createContractEventFilter({
|
||||
address: deployment.addresses.eas as `0x${string}`,
|
||||
abi: contracts.IEAS.abi.abi,
|
||||
eventName: "Attested",
|
||||
fromBlock: 0n,
|
||||
});
|
||||
const events = await publicClient.getFilterLogs({ filter });
|
||||
|
||||
// For each event, check if it's an arbitration request targeting our oracle address
|
||||
// Arbitration requests reference a fulfillment UID which in turn references an escrow UID
|
||||
# Auto mode - uses the LLM specified in the escrow's demand
|
||||
nla escrow:arbitrate --escrow-uid <uid> --auto
|
||||
```
|
||||
|
||||
### 3. Decode escrow and fulfillment data
|
||||
### 2b. Scan for all pending requests
|
||||
|
||||
For a specific escrow UID:
|
||||
To find all unarbitrated fulfillments where the user is the oracle:
|
||||
|
||||
```typescript
|
||||
// Get the escrow attestation
|
||||
const escrow = await client.getAttestation(escrowUid);
|
||||
const escrowData = client.erc20.escrow.nonTierable.decodeObligation(escrow.data);
|
||||
```bash
|
||||
# Interactive mode
|
||||
nla escrow:arbitrate --escrow-uid all
|
||||
|
||||
// The demand is double-encoded:
|
||||
// Layer 1: TrustedOracleArbiter demand (contains oracle address + inner data)
|
||||
const trustedOracleDemand = client.arbiters.general.trustedOracle.decodeDemand(escrowData.demand);
|
||||
// trustedOracleDemand.oracle - the oracle address (should match user's address)
|
||||
// trustedOracleDemand.data - the inner NLA demand data
|
||||
|
||||
// Layer 2: NLA LLM demand (contains provider, model, prompt, demand text)
|
||||
const nlaDemand = llmClient.decodeDemand(trustedOracleDemand.data);
|
||||
// nlaDemand.demand - the natural language demand text
|
||||
// nlaDemand.arbitrationProvider - e.g. "OpenAI"
|
||||
// nlaDemand.arbitrationModel - e.g. "gpt-4o-mini"
|
||||
// nlaDemand.arbitrationPrompt - the prompt template with {{demand}} and {{obligation}} placeholders
|
||||
|
||||
// Get fulfillment data (fulfillments use CommitRevealObligation)
|
||||
const fulfillment = await client.getAttestation(fulfillmentUid);
|
||||
const commitRevealData = client.commitReveal.decode(fulfillment.data);
|
||||
const fulfillmentText = fromHex(commitRevealData.payload, "string");
|
||||
# Auto mode
|
||||
nla escrow:arbitrate --escrow-uid all --auto
|
||||
```
|
||||
|
||||
### 4. Evaluate the fulfillment
|
||||
### 3. Review and decide
|
||||
|
||||
Present the decoded data to the user (or evaluate it yourself as the AI agent):
|
||||
In **interactive mode**, the command displays each pending fulfillment with:
|
||||
- Escrow UID and fulfillment UID
|
||||
- The demand text
|
||||
- The fulfillment text
|
||||
- The arbitration provider/model specified
|
||||
|
||||
- **Demand**: The natural language condition
|
||||
- **Fulfillment**: The submitted text
|
||||
- **Arbitration prompt**: The template that guides evaluation
|
||||
- **Model/Provider**: What was originally specified (informational for manual arbitration)
|
||||
Then prompts for a decision: `approve`, `reject`, or `skip`.
|
||||
|
||||
Apply the arbitration prompt logic: substitute `{{demand}}` and `{{obligation}}` with actual values, then determine if the fulfillment satisfies the demand. The result is a boolean: `true` (approved) or `false` (rejected).
|
||||
In **auto mode** (`--auto`), the command uses the LLM provider/model specified in the escrow's demand to arbitrate automatically. Requires at least one LLM API key via environment variables or flags (`--openai-api-key`, `--anthropic-api-key`, `--openrouter-api-key`).
|
||||
|
||||
### 5. Submit the arbitration decision
|
||||
### 4. Verify
|
||||
|
||||
Use the `arbitrateMany` callback or submit directly:
|
||||
|
||||
```typescript
|
||||
// Option A: Use arbitrateMany with a custom callback (handles polling and submission)
|
||||
const { unwatch } = await client.arbiters.general.trustedOracle.arbitrateMany(
|
||||
async ({ attestation, demand }) => {
|
||||
// decode and evaluate as shown above
|
||||
// return true or false
|
||||
return decision;
|
||||
},
|
||||
{
|
||||
onAfterArbitrate: async (result) => {
|
||||
console.log(`Decision UID: ${result.attestation.uid}`);
|
||||
console.log(`Result: ${result.decision ? "APPROVED" : "REJECTED"}`);
|
||||
},
|
||||
pollingInterval: 1000,
|
||||
}
|
||||
);
|
||||
|
||||
// Option B: For one-shot arbitration, use arbitrateMany with a short polling interval
|
||||
// and call unwatch() after the decision is submitted
|
||||
```
|
||||
|
||||
### 6. Verify the decision
|
||||
After arbitration, check the result:
|
||||
|
||||
```bash
|
||||
nla escrow:status --escrow-uid <escrow_uid>
|
||||
```
|
||||
|
||||
Confirm the arbitration decision appears in the status output.
|
||||
|
||||
## Key details
|
||||
|
||||
- The user's wallet address MUST match the oracle address in the escrow's demand - otherwise the on-chain arbiter contract will reject the decision
|
||||
- Demands are double-encoded: TrustedOracleArbiter wraps NLA LLM demand data
|
||||
- Fulfillments use CommitRevealObligation - decode with `client.commitReveal.decode()`, then `fromHex(payload, "string")`
|
||||
- The `arbitrateMany` method handles both polling for pending requests and submitting decisions on-chain
|
||||
- Each arbitration decision is recorded as an on-chain attestation (permanent and immutable)
|
||||
- Deployment files with contract addresses are at `cli/deployments/<network>.json`
|
||||
- Use `nla network` to check which network is currently active
|
||||
- The user's wallet address MUST match the oracle address in the escrow - otherwise the on-chain contract rejects the decision
|
||||
- Each arbitration decision is recorded as a permanent on-chain attestation
|
||||
- In interactive mode, type `skip` or `s` to skip a fulfillment without deciding
|
||||
- Auto mode reads LLM API keys from environment variables (OPENAI_API_KEY, etc.) or CLI flags
|
||||
- If no pending requests are found, the command explains possible reasons (no fulfillments yet, already arbitrated, or wrong oracle address)
|
||||
|
||||
## Alternative: use nla escrow:status for read-only inspection
|
||||
## Prerequisites
|
||||
|
||||
If the user just wants to inspect escrow state without submitting decisions:
|
||||
- `nla` CLI installed and configured
|
||||
- Private key set via `nla wallet:set`, `--private-key` flag, or `PRIVATE_KEY` env var
|
||||
- ETH in the oracle's account for gas (submitting decisions costs gas)
|
||||
- For auto mode: at least one LLM provider API key
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
# View escrow details, fulfillments, and existing arbitration results
|
||||
nla escrow:status --escrow-uid <uid>
|
||||
```
|
||||
# Scan for all pending requests, decide interactively
|
||||
nla escrow:arbitrate --escrow-uid all
|
||||
|
||||
This does not require being the oracle.
|
||||
# Auto-arbitrate a specific escrow using LLM
|
||||
nla escrow:arbitrate --escrow-uid 0xabc123... --auto
|
||||
|
||||
# Auto-arbitrate all pending, with explicit API key
|
||||
nla escrow:arbitrate --escrow-uid all --auto --openai-api-key sk-...
|
||||
```
|
||||
|
|
|
|||
Loading…
Reference in New Issue