From ffea78080d7d44503c91a0757d592c3c939d322b Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Sun, 15 Mar 2026 13:16:53 +0000 Subject: [PATCH 1/3] [#32] Add agent registration wizard with ERC-8004 integration 3-step wizard at /register-agent: agent profile form with metadata generation, on-chain register(agentURI) call, and EIP-712 signed setAgentWallet binding. Adds register + setAgentWallet ABI entries to erc8004.ts. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/contracts/erc8004.ts | 45 ++- src/app/register-agent/page.tsx | 485 ++++++++++++++++++++++++++++++++ 2 files changed, 524 insertions(+), 6 deletions(-) create mode 100644 src/app/register-agent/page.tsx diff --git a/lib/contracts/erc8004.ts b/lib/contracts/erc8004.ts index f4b6f34d..dd568ad2 100644 --- a/lib/contracts/erc8004.ts +++ b/lib/contracts/erc8004.ts @@ -2,14 +2,16 @@ import { type Address } from "viem"; import { publicClient } from "../viem"; import { ERC8004_REGISTRY } from "./constants"; +// --------------------------------------------------------------------------- +// ABI +// --------------------------------------------------------------------------- + /** - * Minimal ABI for ERC-8004 Agent Registry — reverse lookup by wallet. - * - * `agentIdByWallet(address)` returns the agentId (uint256) for a - * registered agent wallet, or 0 if the address is not a registered - * agent wallet. + * ERC-8004 Agent Registry ABI — agent registration, wallet binding, and + * reverse lookup. */ -const erc8004Abi = [ +export const erc8004Abi = [ + // View { type: "function", name: "agentIdByWallet", @@ -17,6 +19,37 @@ const erc8004Abi = [ inputs: [{ name: "wallet", type: "address" }], outputs: [{ name: "agentId", type: "uint256" }], }, + // Write — register a new agent + { + type: "function", + name: "register", + stateMutability: "nonpayable", + inputs: [{ name: "agentURI", type: "string" }], + outputs: [{ name: "agentId", type: "uint256" }], + }, + // Write — bind a wallet to an agent (EIP-712 signed) + { + type: "function", + name: "setAgentWallet", + stateMutability: "nonpayable", + inputs: [ + { name: "agentId", type: "uint256" }, + { name: "newWallet", type: "address" }, + { name: "signature", type: "bytes" }, + { name: "deadline", type: "uint256" }, + ], + outputs: [], + }, + // Event — emitted on successful registration + { + type: "event", + name: "AgentRegistered", + inputs: [ + { name: "agentId", type: "uint256", indexed: true }, + { name: "owner", type: "address", indexed: true }, + { name: "agentURI", type: "string", indexed: false }, + ], + }, ] as const; /** diff --git a/src/app/register-agent/page.tsx b/src/app/register-agent/page.tsx new file mode 100644 index 00000000..e2a690ea --- /dev/null +++ b/src/app/register-agent/page.tsx @@ -0,0 +1,485 @@ +"use client"; + +import { useState, useMemo } from "react"; +import { useAccount, useWriteContract, useSignTypedData } from "wagmi"; +import { decodeEventLog, type Hex } from "viem"; +import { useRouter } from "next/navigation"; +import { publicClient } from "../../../lib/rpc"; +import { erc8004Abi } from "../../../lib/contracts/erc8004"; +import { ERC8004_REGISTRY, BASE_CHAIN_ID } from "../../../lib/contracts/constants"; +import { ConnectWallet } from "../../components/ConnectWallet"; + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +const GENRES = [ + "Fantasy", + "Sci-Fi", + "Mystery", + "Romance", + "Horror", + "Thriller", + "Literary Fiction", + "Comedy", + "Historical", + "Adventure", +] as const; + +const LLM_MODELS = [ + "Claude Opus 4", + "Claude Sonnet 4", + "GPT-5", + "GPT-4o", + "Gemini 2.5 Pro", + "Llama 4 Maverick", + "Custom / Other", +] as const; + +type WizardStep = 1 | 2 | 3; + +// EIP-712 domain for setAgentWallet +const EIP712_DOMAIN = { + name: "ERC8004AgentRegistry", + version: "1", + chainId: BigInt(BASE_CHAIN_ID), + verifyingContract: ERC8004_REGISTRY, +} as const; + +const SET_WALLET_TYPES = { + SetAgentWallet: [ + { name: "agentId", type: "uint256" }, + { name: "newWallet", type: "address" }, + { name: "deadline", type: "uint256" }, + ], +} as const; + +// --------------------------------------------------------------------------- +// Component +// --------------------------------------------------------------------------- + +export default function RegisterAgentPage() { + const router = useRouter(); + const { address, isConnected } = useAccount(); + const { writeContractAsync } = useWriteContract(); + const { signTypedDataAsync } = useSignTypedData(); + + // Step tracking + const [step, setStep] = useState(1); + + // Step 1 — profile form + const [name, setName] = useState(""); + const [description, setDescription] = useState(""); + const [genre, setGenre] = useState(""); + const [llmModel, setLlmModel] = useState(""); + + // Step 2 — registration + const [registering, setRegistering] = useState(false); + const [regTxHash, setRegTxHash] = useState(); + const [agentId, setAgentId] = useState(); + + // Step 3 — wallet binding + const [agentWallet, setAgentWallet] = useState(""); + const [binding, setBinding] = useState(false); + const [bindTxHash, setBindTxHash] = useState(); + + // Error state + const [error, setError] = useState(null); + + // Derived: metadata JSON + const agentURI = useMemo(() => { + if (!name.trim()) return ""; + const metadata = { + name: name.trim(), + description: description.trim(), + genre: genre || undefined, + llmModel: llmModel || undefined, + registeredBy: address, + registeredAt: new Date().toISOString(), + }; + return JSON.stringify(metadata); + }, [name, description, genre, llmModel, address]); + + const profileValid = name.trim().length > 0 && description.trim().length > 0; + + // ------------------------------------------------------------------------- + // Connect gate + // ------------------------------------------------------------------------- + + if (!isConnected) { + return ( +
+

+ Connect your wallet to register an agent. +

+ +
+ ); + } + + // ------------------------------------------------------------------------- + // Step 2 handler — register(agentURI) + // ------------------------------------------------------------------------- + + async function handleRegister() { + try { + setError(null); + setRegistering(true); + + const hash = await writeContractAsync({ + address: ERC8004_REGISTRY, + abi: erc8004Abi, + functionName: "register", + args: [agentURI], + }); + setRegTxHash(hash); + + const receipt = await publicClient.waitForTransactionReceipt({ hash }); + + // Parse AgentRegistered event to extract agentId + const registeredLog = receipt.logs.find((log) => { + try { + const decoded = decodeEventLog({ + abi: erc8004Abi, + data: log.data, + topics: log.topics, + }); + return decoded.eventName === "AgentRegistered"; + } catch { + return false; + } + }); + + if (registeredLog) { + const decoded = decodeEventLog({ + abi: erc8004Abi, + data: registeredLog.data, + topics: registeredLog.topics, + }); + if (decoded.eventName === "AgentRegistered") { + setAgentId(decoded.args.agentId); + } + } + + setStep(3); + } catch (err) { + setError(err instanceof Error ? err.message : "Registration failed"); + } finally { + setRegistering(false); + } + } + + // ------------------------------------------------------------------------- + // Step 3 handler — EIP-712 sign + setAgentWallet + // ------------------------------------------------------------------------- + + async function handleSetWallet() { + if (!agentId || !agentWallet) return; + + try { + setError(null); + setBinding(true); + + // Deadline: 1 hour from now + const deadline = BigInt(Math.floor(Date.now() / 1000) + 3600); + + const signature = await signTypedDataAsync({ + domain: EIP712_DOMAIN, + types: SET_WALLET_TYPES, + primaryType: "SetAgentWallet", + message: { + agentId, + newWallet: agentWallet as `0x${string}`, + deadline, + }, + }); + + const hash = await writeContractAsync({ + address: ERC8004_REGISTRY, + abi: erc8004Abi, + functionName: "setAgentWallet", + args: [agentId, agentWallet as `0x${string}`, signature, deadline], + }); + setBindTxHash(hash); + + await publicClient.waitForTransactionReceipt({ hash }); + + // Redirect to create storyline flow + router.push("/create"); + } catch (err) { + setError(err instanceof Error ? err.message : "Wallet binding failed"); + } finally { + setBinding(false); + } + } + + // ------------------------------------------------------------------------- + // Render + // ------------------------------------------------------------------------- + + return ( +
+

+ Register Agent +

+

+ Register an AI agent writer on the ERC-8004 Agent Registry. +

+ + {/* Step indicator */} +
+ {([1, 2, 3] as const).map((s) => ( +
+
+ {s < step ? "\u2713" : s} +
+ {s < 3 && ( +
+ )} +
+ ))} + + {step === 1 && "Agent Profile"} + {step === 2 && "On-chain Registration"} + {step === 3 && "Bind Wallet"} + +
+ + {/* Error banner */} + {error && ( +
+ {error} +
+ )} + + {/* ----------------------------------------------------------------- */} + {/* Step 1: Profile Form */} + {/* ----------------------------------------------------------------- */} + {step === 1 && ( +
{ + e.preventDefault(); + if (profileValid) setStep(2); + }} + className="mt-8 space-y-6" + > + {/* Name */} +
+ + setName(e.target.value)} + placeholder="e.g. Plotweaver-7B" + className="border-border bg-surface text-foreground placeholder:text-muted w-full rounded border px-3 py-2 text-sm focus:border-accent focus:outline-none" + /> +
+ + {/* Description */} +
+ +