Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 100 additions & 1 deletion lib/contracts/erc8004.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,68 @@ export interface AgentMetadata {
}

export const erc8004Abi = [
// View
// View — reverse lookup wallet → agentId
{
type: "function",
name: "agentIdByWallet",
stateMutability: "view",
inputs: [{ name: "wallet", type: "address" }],
outputs: [{ name: "agentId", type: "uint256" }],
},
// View — fetch metadata URI for an agent
{
type: "function",
name: "agentURI",
stateMutability: "view",
inputs: [{ name: "agentId", type: "uint256" }],
outputs: [{ name: "", type: "string" }],
},
// View — get the bound agent wallet for an agentId
{
type: "function",
name: "getAgentWallet",
stateMutability: "view",
inputs: [{ name: "agentId", type: "uint256" }],
outputs: [{ name: "", type: "address" }],
},
// View — get arbitrary metadata by key
{
type: "function",
name: "getMetadata",
stateMutability: "view",
inputs: [
{ name: "agentId", type: "uint256" },
{ name: "metadataKey", type: "string" },
],
outputs: [{ name: "", type: "bytes" }],
},
// View — ERC-721: owner of a token (agentId)
{
type: "function",
name: "ownerOf",
stateMutability: "view",
inputs: [{ name: "tokenId", type: "uint256" }],
outputs: [{ name: "owner", type: "address" }],
},
// View — ERC-721: number of tokens owned by an address
{
type: "function",
name: "balanceOf",
stateMutability: "view",
inputs: [{ name: "owner", type: "address" }],
outputs: [{ name: "", type: "uint256" }],
},
// View — ERC-721 Enumerable: token at index for owner
{
type: "function",
name: "tokenOfOwnerByIndex",
stateMutability: "view",
inputs: [
{ name: "owner", type: "address" },
{ name: "index", type: "uint256" },
],
outputs: [{ name: "", type: "uint256" }],
},
// Write — register a new agent
{
type: "function",
Expand All @@ -56,6 +103,37 @@ export const erc8004Abi = [
],
outputs: [],
},
// Write — remove agent wallet binding
{
type: "function",
name: "unsetAgentWallet",
stateMutability: "nonpayable",
inputs: [{ name: "agentId", type: "uint256" }],
outputs: [],
},
// Write — update agent URI (owner/approved only)
{
type: "function",
name: "setAgentURI",
stateMutability: "nonpayable",
inputs: [
{ name: "agentId", type: "uint256" },
{ name: "newURI", type: "string" },
],
outputs: [],
},
// Write — set arbitrary metadata key/value
{
type: "function",
name: "setMetadata",
stateMutability: "nonpayable",
inputs: [
{ name: "agentId", type: "uint256" },
{ name: "metadataKey", type: "string" },
{ name: "metadataValue", type: "bytes" },
],
outputs: [],
},
// Event — emitted on successful registration
{
type: "event",
Expand All @@ -66,6 +144,27 @@ export const erc8004Abi = [
{ name: "owner", type: "address", indexed: true },
],
},
// Event — emitted when URI is updated
{
type: "event",
name: "URIUpdated",
inputs: [
{ name: "agentId", type: "uint256", indexed: true },
{ name: "newURI", type: "string", indexed: false },
{ name: "updatedBy", type: "address", indexed: true },
],
},
// Event — emitted when metadata is set
{
type: "event",
name: "MetadataSet",
inputs: [
{ name: "agentId", type: "uint256", indexed: true },
{ name: "indexedMetadataKey", type: "string", indexed: true },
{ name: "metadataKey", type: "string", indexed: false },
{ name: "metadataValue", type: "bytes", indexed: false },
],
},
] as const;

/**
Expand Down
66 changes: 62 additions & 4 deletions src/app/agents/page.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,70 @@
"use client";

import { useState } from "react";
import { useAccount } from "wagmi";
import { useAccount, useReadContract } from "wagmi";
import { zeroAddress } from "viem";

Check warning on line 5 in src/app/agents/page.tsx

View workflow job for this annotation

GitHub Actions / lint-and-typecheck

'zeroAddress' is defined but never used
import { ConnectWallet } from "../../components/ConnectWallet";
import { AgentRegister } from "../../components/AgentRegister";
import { AgentManage } from "../../components/AgentManage";
import { AgentBuild } from "../../components/AgentBuild";
import { AgentDashboard } from "../../components/AgentDashboard";
import { erc8004Abi } from "../../../lib/contracts/erc8004";
import { ERC8004_REGISTRY } from "../../../lib/contracts/constants";

type Tab = "register" | "build" | "dashboard";

export default function AgentsPage() {
const { isConnected } = useAccount();
const { isConnected, address } = useAccount();
const [tab, setTab] = useState<Tab>("register");

// Check if wallet is bound as an agent wallet
const { data: agentIdByWallet, isLoading: walletLoading } = useReadContract({
address: ERC8004_REGISTRY,
abi: erc8004Abi,
functionName: "agentIdByWallet",
args: address ? [address] : undefined,
query: { enabled: !!address },
});

// Check if wallet owns any agent NFTs (ERC-721 balanceOf)
const { data: nftBalance, isLoading: balanceLoading } = useReadContract({
address: ERC8004_REGISTRY,
abi: erc8004Abi,
functionName: "balanceOf",
args: address ? [address] : undefined,
query: { enabled: !!address },
});

// If owner, get the first owned token ID
const hasNft = nftBalance !== undefined && nftBalance > BigInt(0);
const { data: ownedTokenId, isLoading: tokenLoading } = useReadContract({
address: ERC8004_REGISTRY,
abi: erc8004Abi,
functionName: "tokenOfOwnerByIndex",
args: address ? [address, BigInt(0)] : undefined,
query: { enabled: !!address && hasNft },
});

const isAgentWallet = agentIdByWallet !== undefined && agentIdByWallet > BigInt(0);
const isOwner = hasNft && ownedTokenId !== undefined;
const detectLoading = walletLoading || balanceLoading || (hasNft && tokenLoading);

// Determine which agentId and role to use
let detectedAgentId: bigint | undefined;
let detectedRole: "owner" | "agentWallet" | undefined;
if (isOwner) {
detectedAgentId = ownedTokenId;
detectedRole = "owner";
} else if (isAgentWallet) {
detectedAgentId = agentIdByWallet;
detectedRole = "agentWallet";
}

const hasExistingAgent = detectedAgentId !== undefined && detectedRole !== undefined;

// Determine the label for the first tab
const firstTabLabel = hasExistingAgent ? "Manage" : "Register";

return (
<div className="mx-auto max-w-2xl px-6 py-12">
<h1 className="font-body text-2xl font-bold tracking-tight text-accent">
Expand All @@ -34,7 +86,7 @@
: "text-muted hover:text-foreground"
}`}
>
{t === "register" ? "Register" : t === "build" ? "Build" : "Dashboard"}
{t === "register" ? firstTabLabel : t === "build" ? "Build" : "Dashboard"}
</button>
))}
</div>
Expand All @@ -43,9 +95,15 @@
{tab === "register" && (
!isConnected ? (
<div className="flex flex-col items-center justify-center gap-4 py-16">
<p className="text-muted text-sm">Connect your wallet to register an agent.</p>
<p className="text-muted text-sm">Connect your wallet to register or manage an agent.</p>
<ConnectWallet />
</div>
) : detectLoading ? (
<div className="mt-6 py-8 text-center">
<p className="text-muted text-sm">Detecting agent status...</p>
</div>
) : hasExistingAgent ? (
<AgentManage agentId={detectedAgentId!} role={detectedRole!} />
) : (
<AgentRegister />
)
Expand Down
66 changes: 56 additions & 10 deletions src/components/AgentDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,69 @@ import { ERC8004_REGISTRY } from "../../lib/contracts/constants";
export function AgentDashboard() {
const { address } = useAccount();

// Check if wallet is registered as an agent
const { data: agentId, isLoading: agentIdLoading } = useReadContract({
// Check if wallet is registered as an agent wallet
const { data: agentIdByWallet, isLoading: walletLoading } = useReadContract({
address: ERC8004_REGISTRY,
abi: erc8004Abi,
functionName: "agentIdByWallet",
args: address ? [address] : undefined,
query: { enabled: !!address },
});

const isAgent = agentId !== undefined && agentId > BigInt(0);
// Check if wallet owns agent NFTs (owner role)
const { data: nftBalance, isLoading: balanceLoading } = useReadContract({
address: ERC8004_REGISTRY,
abi: erc8004Abi,
functionName: "balanceOf",
args: address ? [address] : undefined,
query: { enabled: !!address },
});

const hasNft = nftBalance !== undefined && nftBalance > BigInt(0);
const { data: ownedTokenId, isLoading: tokenLoading } = useReadContract({
address: ERC8004_REGISTRY,
abi: erc8004Abi,
functionName: "tokenOfOwnerByIndex",
args: address ? [address, BigInt(0)] : undefined,
query: { enabled: !!address && hasNft },
});

// Get the agent wallet for the owned token (to query storylines by that address)
const isOwner = hasNft && ownedTokenId !== undefined;
const { data: boundAgentWallet } = useReadContract({
address: ERC8004_REGISTRY,
abi: erc8004Abi,
functionName: "getAgentWallet",
args: ownedTokenId !== undefined ? [ownedTokenId] : undefined,
query: { enabled: isOwner },
});

const isAgentWallet = agentIdByWallet !== undefined && agentIdByWallet > BigInt(0);
const agentId = isOwner ? ownedTokenId : isAgentWallet ? agentIdByWallet : undefined;
const isAgent = agentId !== undefined;

// Determine the writer address for storyline lookup
// If connected as owner, prefer the bound agent wallet but fall back to owner address
// when the agent wallet is unset (zero address) or missing
const hasValidAgentWallet =
boundAgentWallet && boundAgentWallet !== "0x0000000000000000000000000000000000000000";
const writerAddress = isOwner
? hasValidAgentWallet ? (boundAgentWallet as string) : address
: address;

// Fetch agent's storylines from Supabase
const { data: storylines, isLoading: storylinesLoading } = useQuery({
queryKey: ["agent-storylines", address],
queryKey: ["agent-storylines", writerAddress],
queryFn: async () => {
if (!address) return [];
const res = await fetch(`/api/storyline/by-writer?writer=${address}&type=agent`);
if (!writerAddress) return [];
const res = await fetch(`/api/storyline/by-writer?writer=${writerAddress}&type=agent`);
if (!res.ok) return [];
return res.json() as Promise<Array<{ storyline_id: number; title: string; token_address: string; plot_count: number }>>;
},
enabled: !!address && isAgent,
enabled: !!writerAddress && isAgent,
});

if (agentIdLoading) {
if (walletLoading || balanceLoading || (hasNft && tokenLoading)) {
return (
<div className="mt-6 py-8 text-center">
<p className="text-muted text-sm">Loading agent status...</p>
Expand All @@ -54,8 +93,15 @@ export function AgentDashboard() {
return (
<div className="mt-6">
<div className="border-accent/30 bg-accent/5 rounded border px-4 py-3 mb-6">
<p className="text-accent text-sm font-medium">Agent #{agentId.toString()}</p>
<p className="text-muted mt-1 text-xs font-mono">
<p className="text-accent text-sm font-medium">Agent #{agentId!.toString()}</p>
<p className="text-muted mt-1 text-xs">
{isOwner && isAgentWallet
? "Connected as owner + agent wallet"
: isOwner
? "Connected as owner"
: "Connected as agent wallet"}
</p>
<p className="text-muted text-xs font-mono">
{address?.slice(0, 6)}...{address?.slice(-4)}
</p>
</div>
Expand Down
Loading
Loading