natural-language-agreements/demo/demo.ts

422 lines
19 KiB
TypeScript

#!/usr/bin/env bun
/**
* NLA Oracle Demo — Content Creation Bounty
*
* Demonstrates AI-arbitrated escrows on Base Sepolia.
* Uses two wallets: Alice (escrow creator) and Bob (fulfiller).
*
* Usage:
* bun run demo/demo.ts create — Deploy test token + create escrow
* bun run demo/demo.ts fulfill — Bob submits his blog post
* bun run demo/demo.ts status — Check escrow + arbitration status
* bun run demo/demo.ts collect — Bob collects tokens (if approved)
*/
import { createWalletClient, createPublicClient, http, publicActions, formatEther, toHex, keccak256, parseAbi } from "viem";
import { privateKeyToAccount, generatePrivateKey } from "viem/accounts";
import { baseSepolia } from "viem/chains";
import { makeClient } from "alkahest-ts";
import { contractAddresses, fixtures } from "alkahest-ts";
import { makeLLMClient } from "..";
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
import { join, dirname } from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// ─── Config ──────────────────────────────────────────────────────
const RPC_URL = "https://sepolia.base.org";
const ORACLE_ADDRESS = "0x2d2E0a49B733E3CBB2B6C04C417aa5E24cd2A70F" as const;
const ADDRESSES = contractAddresses["Base Sepolia"];
const STATE_FILE = join(__dirname, ".demo-state.json");
// ─── Demo content ────────────────────────────────────────────────
const DEMAND = `Write a blog post of at least 200 words explaining how regenerative economics differs from extractive capitalism. Include at least one real-world example of a regenerative economic project.`;
const FULFILLMENT = `# Regenerative Economics: Beyond Extraction
Traditional extractive capitalism operates on a linear model: extract resources, produce goods, consume, and discard. This approach treats natural and social capital as externalities — costs borne by communities and ecosystems rather than by the businesses profiting from them. The result is a system that concentrates wealth while depleting the very foundations it depends on.
Regenerative economics flips this paradigm. Instead of extracting value until resources are depleted, regenerative models aim to create conditions for life to continuously renew itself. This means designing economic systems that restore ecosystems, strengthen communities, and distribute value more equitably.
A powerful real-world example is the Mondragon Corporation in Spain's Basque Country. Founded in 1956, Mondragon is a federation of worker cooperatives employing over 80,000 people. Profits are shared among worker-owners, decision-making is democratic, and a portion of earnings is reinvested into education, community development, and new cooperative ventures. Unlike extractive corporations that funnel profits to distant shareholders, Mondragon's wealth circulates locally, building resilience and shared prosperity.
The key insight of regenerative economics is that an economy is not separate from the living systems it inhabits — it IS a living system, and must be designed accordingly.`;
// ─── Helpers ─────────────────────────────────────────────────────
function loadState(): Record<string, any> {
if (existsSync(STATE_FILE)) {
return JSON.parse(readFileSync(STATE_FILE, "utf-8"));
}
return {};
}
function saveState(state: Record<string, any>) {
writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
}
function makeWallet(privateKey: string) {
const account = privateKeyToAccount(privateKey as `0x${string}`);
return createWalletClient({
account,
chain: baseSepolia,
transport: http(RPC_URL),
}).extend(publicActions);
}
// Simple ERC20 bytecode for deploying a test token
const MOCK_TOKEN_ABI = parseAbi([
"constructor(string name, string symbol, uint256 initialSupply)",
"function transfer(address to, uint256 amount) returns (bool)",
"function balanceOf(address owner) view returns (uint256)",
"function approve(address spender, uint256 amount) returns (bool)",
"function allowance(address owner, address spender) view returns (uint256)",
"function name() view returns (string)",
"function symbol() view returns (string)",
"function decimals() view returns (uint8)",
"function totalSupply() view returns (uint256)",
]);
// ─── Commands ────────────────────────────────────────────────────
async function setup() {
let state = loadState();
if (!state.aliceKey) {
state.aliceKey = generatePrivateKey();
console.log("Generated Alice's wallet key");
}
if (!state.bobKey) {
state.bobKey = generatePrivateKey();
console.log("Generated Bob's wallet key");
}
const alice = privateKeyToAccount(state.aliceKey as `0x${string}`);
const bob = privateKeyToAccount(state.bobKey as `0x${string}`);
saveState(state);
const publicClient = createPublicClient({
chain: baseSepolia,
transport: http(RPC_URL),
});
const aliceBal = await publicClient.getBalance({ address: alice.address });
const bobBal = await publicClient.getBalance({ address: bob.address });
const oracleBal = await publicClient.getBalance({ address: ORACLE_ADDRESS });
console.log("\n═══════════════════════════════════════════════════════");
console.log(" NLA Demo — Wallet Setup");
console.log("═══════════════════════════════════════════════════════\n");
console.log(` Alice (escrow creator): ${alice.address}`);
console.log(` ETH: ${formatEther(aliceBal)}`);
console.log(` Needs: ~0.01 ETH for token deploy + escrow creation\n`);
console.log(` Bob (fulfiller): ${bob.address}`);
console.log(` ETH: ${formatEther(bobBal)}`);
console.log(` Needs: ~0.005 ETH for fulfillment tx\n`);
console.log(` Oracle: ${ORACLE_ADDRESS}`);
console.log(` ETH: ${formatEther(oracleBal)}`);
console.log(` Needs: ~0.005 ETH for arbitration tx\n`);
const unfunded = [];
if (aliceBal === 0n) unfunded.push(`Alice: ${alice.address}`);
if (bobBal === 0n) unfunded.push(`Bob: ${bob.address}`);
if (oracleBal === 0n) unfunded.push(`Oracle: ${ORACLE_ADDRESS}`);
if (unfunded.length > 0) {
console.log(" ⚠️ Fund these wallets with Base Sepolia ETH:");
unfunded.forEach(w => console.log(`${w}`));
console.log("\n Faucets:");
console.log(" https://www.alchemy.com/faucets/base-sepolia");
console.log(" https://portal.cdp.coinbase.com/products/faucet");
} else {
console.log(" ✅ All wallets funded! Run: bun run demo/demo.ts create");
}
console.log("═══════════════════════════════════════════════════════\n");
}
async function create() {
const state = loadState();
if (!state.aliceKey) {
console.error("Run 'bun run demo/demo.ts setup' first");
process.exit(1);
}
const alice = makeWallet(state.aliceKey);
const balance = await alice.getBalance({ address: alice.account.address });
if (balance === 0n) {
console.error(`❌ Alice has no ETH. Fund ${alice.account.address} first.`);
process.exit(1);
}
console.log("\n═══════════════════════════════════════════════════════");
console.log(" Step 1: Deploy Test Token + Create Escrow");
console.log("═══════════════════════════════════════════════════════\n");
// Deploy a simple test ERC20 using the MockERC20Permit from alkahest fixtures
console.log("📝 Deploying test token (BOUNTY)...");
const deployHash = await alice.deployContract({
abi: fixtures.MockERC20Permit.abi,
bytecode: fixtures.MockERC20Permit.bytecode.object as `0x${string}`,
args: ["Bounty Token", "BOUNTY"],
});
const deployReceipt = await alice.waitForTransactionReceipt({ hash: deployHash });
const tokenAddress = deployReceipt.contractAddress!;
console.log(` ✅ Token deployed: ${tokenAddress}`);
// Mint tokens to Alice
console.log("💰 Minting 10,000 BOUNTY to Alice...");
const mintHash = await alice.writeContract({
address: tokenAddress,
abi: fixtures.MockERC20Permit.abi,
functionName: "mint",
args: [alice.account.address, 10000n],
});
await alice.waitForTransactionReceipt({ hash: mintHash });
console.log(" ✅ Minted");
// Create alkahest client
const client = makeClient(alice as any, ADDRESSES);
const llmClient = client.extend(() => ({
llm: makeLLMClient([]),
}));
// Encode the demand
console.log("\n📋 Creating escrow...");
console.log(` Demand: "${DEMAND.slice(0, 80)}..."`);
console.log(` Amount: 1000 BOUNTY`);
console.log(` Oracle: ${ORACLE_ADDRESS}`);
console.log(` Arbitrator: Anthropic Claude`);
const encodedDemand = client.arbiters.general.trustedOracle.encodeDemand({
oracle: ORACLE_ADDRESS,
data: llmClient.llm.encodeDemand({
arbitrationProvider: "Anthropic",
arbitrationModel: "claude-3-5-sonnet-20241022",
arbitrationPrompt: `You are evaluating whether a blog post fulfills a content bounty.
Evaluate the submission against the demand criteria carefully.
Demand: {{demand}}
Submission: {{obligation}}
Does the submission meet ALL the criteria in the demand? Respond with only 'true' or 'false'.`,
demand: DEMAND,
}),
}) as `0x${string}`;
const arbiter = ADDRESSES.trustedOracleArbiter as `0x${string}`;
const { attested: escrow } = await client.erc20.escrow.nonTierable.permitAndCreate(
{ address: tokenAddress, value: 1000n },
{ arbiter, demand: encodedDemand },
0n,
);
console.log(`\n ✅ Escrow created!`);
console.log(` Escrow UID: ${escrow.uid}`);
// Save state
state.tokenAddress = tokenAddress;
state.escrowUid = escrow.uid;
saveState(state);
console.log("\n═══════════════════════════════════════════════════════");
console.log(" Next: bun run demo/demo.ts fulfill");
console.log("═══════════════════════════════════════════════════════\n");
}
async function fulfill() {
const state = loadState();
if (!state.escrowUid || !state.bobKey) {
console.error("Run 'create' step first");
process.exit(1);
}
const bob = makeWallet(state.bobKey);
const balance = await bob.getBalance({ address: bob.account.address });
if (balance === 0n) {
console.error(`❌ Bob has no ETH. Fund ${bob.account.address} first.`);
process.exit(1);
}
console.log("\n═══════════════════════════════════════════════════════");
console.log(" Step 2: Bob Submits Fulfillment");
console.log("═══════════════════════════════════════════════════════\n");
console.log(`📝 Submitting blog post (${FULFILLMENT.split(/\s+/).length} words)...\n`);
console.log(` "${FULFILLMENT.slice(0, 100)}..."\n`);
const client = makeClient(bob as any, ADDRESSES);
// Commit-reveal flow
const schema = keccak256(toHex("{item:string}"));
const salt = keccak256(toHex(crypto.randomUUID()));
const payload = toHex(FULFILLMENT);
const obligationData = { payload, salt, schema };
console.log("🔒 Step 2a: Computing commitment...");
const commitment = await client.commitReveal.computeCommitment(
state.escrowUid as `0x${string}`,
bob.account.address,
obligationData,
);
console.log("📝 Step 2b: Submitting commitment (with bond)...");
const { hash: commitHash } = await client.commitReveal.commit(commitment);
await bob.waitForTransactionReceipt({ hash: commitHash });
console.log("🔓 Step 2c: Revealing obligation...");
const { attested: fulfillmentAttestation } = await client.commitReveal.doObligation(
obligationData,
state.escrowUid as `0x${string}`,
);
console.log("💰 Step 2d: Reclaiming bond...");
await client.commitReveal.reclaimBond(fulfillmentAttestation.uid);
console.log(`\n ✅ Fulfillment submitted!`);
console.log(` Fulfillment UID: ${fulfillmentAttestation.uid}`);
// Request arbitration
console.log("\n📤 Requesting arbitration from oracle...");
const escrow = await client.getAttestation(state.escrowUid as `0x${string}`);
const decodedEscrow = client.erc20.escrow.nonTierable.decodeObligation(escrow.data);
await client.arbiters.general.trustedOracle.requestArbitration(
fulfillmentAttestation.uid,
ORACLE_ADDRESS,
decodedEscrow.demand,
);
console.log(" ✅ Arbitration requested!");
state.fulfillmentUid = fulfillmentAttestation.uid;
saveState(state);
console.log("\n═══════════════════════════════════════════════════════");
console.log(" The oracle will now evaluate the blog post using Claude.");
console.log(" Check: docker logs nla-oracle --tail 20");
console.log(" Then: bun run demo/demo.ts status");
console.log("═══════════════════════════════════════════════════════\n");
}
async function status() {
const state = loadState();
if (!state.escrowUid) {
console.error("No escrow found. Run 'create' first.");
process.exit(1);
}
// Use Alice's wallet just for reading
const alice = makeWallet(state.aliceKey);
const client = makeClient(alice as any, ADDRESSES);
console.log("\n═══════════════════════════════════════════════════════");
console.log(" Escrow Status");
console.log("═══════════════════════════════════════════════════════\n");
const escrow = await client.getAttestation(state.escrowUid as `0x${string}`);
const decoded = client.erc20.escrow.nonTierable.decodeObligation(escrow.data);
console.log(` Escrow UID: ${state.escrowUid}`);
console.log(` Token: ${state.tokenAddress}`);
console.log(` Amount: ${decoded.token.value.toString()} BOUNTY`);
console.log(` Revoked: ${escrow.revocationTime > 0n ? "Yes (collected!)" : "No (still locked)"}`);
if (state.fulfillmentUid) {
console.log(`\n Fulfillment: ${state.fulfillmentUid}`);
console.log(` Status: Submitted, awaiting/completed arbitration`);
console.log(`\n Check oracle logs: docker logs nla-oracle --tail 30`);
} else {
console.log(`\n No fulfillment submitted yet.`);
}
console.log("═══════════════════════════════════════════════════════\n");
}
async function collect() {
const state = loadState();
if (!state.fulfillmentUid || !state.bobKey) {
console.error("Run 'fulfill' step first");
process.exit(1);
}
const bob = makeWallet(state.bobKey);
const client = makeClient(bob as any, ADDRESSES);
console.log("\n═══════════════════════════════════════════════════════");
console.log(" Step 3: Bob Collects Tokens");
console.log("═══════════════════════════════════════════════════════\n");
try {
console.log("💰 Collecting escrow tokens...");
await client.erc20.escrow.nonTierable.collectPayment(
state.escrowUid as `0x${string}`,
state.fulfillmentUid as `0x${string}`,
);
// Check Bob's token balance
const tokenBalance = await bob.readContract({
address: state.tokenAddress as `0x${string}`,
abi: fixtures.MockERC20Permit.abi,
functionName: "balanceOf",
args: [bob.account.address],
}) as bigint;
console.log(`\n ✅ Tokens collected!`);
console.log(` Bob's BOUNTY balance: ${tokenBalance.toString()}`);
console.log("\n The AI oracle approved Bob's blog post and");
console.log(" released the escrowed tokens automatically.");
state.collected = true;
saveState(state);
} catch (error: any) {
if (error.message?.includes("reverted")) {
console.log(" ⏳ Arbitration not yet complete or was rejected.");
console.log(" Check oracle logs: docker logs nla-oracle --tail 30");
console.log(" Then try again.");
} else {
throw error;
}
}
console.log("═══════════════════════════════════════════════════════\n");
}
// ─── CLI router ──────────────────────────────────────────────────
const command = process.argv[2] || Bun.argv[2];
switch (command) {
case "setup":
await setup();
break;
case "create":
await setup(); // ensure wallets exist
await create();
break;
case "fulfill":
await fulfill();
break;
case "status":
await status();
break;
case "collect":
await collect();
break;
default:
console.log(`
NLA Oracle Demo — Content Creation Bounty
Usage:
bun run demo/demo.ts setup Generate wallets & show funding instructions
bun run demo/demo.ts create Deploy test token + create escrow
bun run demo/demo.ts fulfill Bob submits his blog post
bun run demo/demo.ts status Check escrow & arbitration status
bun run demo/demo.ts collect Bob collects tokens (if approved)
`);
}