From 77e018870c2655315e49def941e33cc67c8eae9b Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Tue, 24 Mar 2026 16:09:38 +0000 Subject: [PATCH 1/4] [#501] Add profile page with identity resolution and sitewide linking - Create /profile/[address] route with header identity (Farcaster + ERC-8004 agent metadata), Human/AI badge, and tabs for Stories, Portfolio, and Activity - Add getAgentMetadata() to lib/contracts/erc8004.ts for resolving on-chain agent name, description, model, genre - Add fetchAgentMetadata() server action to lib/actions.ts - Update WriterIdentity, WriterIdentityClient, and FarcasterAvatar to link to /profile/[address] instead of external Farcaster - Link donor addresses in writer dashboard to profile pages Fixes #501 Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/actions.ts | 14 + lib/contracts/erc8004.ts | 54 +++ src/app/dashboard/writer/page.tsx | 7 +- src/app/profile/[address]/page.tsx | 447 ++++++++++++++++++++++++ src/components/FarcasterAvatar.tsx | 48 ++- src/components/WriterIdentity.tsx | 27 +- src/components/WriterIdentityClient.tsx | 42 ++- 7 files changed, 596 insertions(+), 43 deletions(-) create mode 100644 src/app/profile/[address]/page.tsx diff --git a/lib/actions.ts b/lib/actions.ts index dec1acbb..8d5a3829 100644 --- a/lib/actions.ts +++ b/lib/actions.ts @@ -1,6 +1,11 @@ "use server"; import { lookupByAddress, type FarcasterProfile } from "./farcaster"; +import { + getAgentMetadata as _getAgentMetadata, + type AgentMetadata, +} from "./contracts/erc8004"; +import type { Address } from "viem"; /** * Server action that resolves an Ethereum address to a Farcaster profile. @@ -11,3 +16,12 @@ export async function getFarcasterProfile( ): Promise { return lookupByAddress(address); } + +/** + * Server action that resolves ERC-8004 agent metadata from a wallet address. + */ +export async function fetchAgentMetadata( + address: string, +): Promise { + return _getAgentMetadata(address as Address); +} diff --git a/lib/contracts/erc8004.ts b/lib/contracts/erc8004.ts index 0179c941..bad58a4f 100644 --- a/lib/contracts/erc8004.ts +++ b/lib/contracts/erc8004.ts @@ -10,6 +10,15 @@ import { ERC8004_REGISTRY } from "./constants"; * ERC-8004 Agent Registry ABI — agent registration, wallet binding, and * reverse lookup. */ +export interface AgentMetadata { + name: string; + description: string; + genre?: string; + llmModel?: string; + registeredBy?: string; + registeredAt?: string; +} + export const erc8004Abi = [ // View { @@ -19,6 +28,13 @@ export const erc8004Abi = [ inputs: [{ name: "wallet", type: "address" }], outputs: [{ name: "agentId", type: "uint256" }], }, + { + type: "function", + name: "agentURI", + stateMutability: "view", + inputs: [{ name: "agentId", type: "uint256" }], + outputs: [{ name: "", type: "string" }], + }, // Write — register a new agent { type: "function", @@ -77,3 +93,41 @@ export async function detectWriterType( return 0; } } + +/** + * Resolve ERC-8004 agent metadata from an Ethereum address. + * Returns null if the address is not a registered agent or on any error. + */ +export async function getAgentMetadata( + walletAddress: Address, +): Promise { + try { + const agentId = await publicClient.readContract({ + address: ERC8004_REGISTRY, + abi: erc8004Abi, + functionName: "agentIdByWallet", + args: [walletAddress], + }); + if (agentId <= BigInt(0)) return null; + + const uri = await publicClient.readContract({ + address: ERC8004_REGISTRY, + abi: erc8004Abi, + functionName: "agentURI", + args: [agentId], + }); + if (!uri) return null; + + const parsed = JSON.parse(uri as string) as Record; + return { + name: (parsed.name as string) || "Unknown Agent", + description: (parsed.description as string) || "", + genre: (parsed.genre as string) || undefined, + llmModel: (parsed.llmModel as string) || undefined, + registeredBy: (parsed.registeredBy as string) || undefined, + registeredAt: (parsed.registeredAt as string) || undefined, + }; + } catch { + return null; + } +} diff --git a/src/app/dashboard/writer/page.tsx b/src/app/dashboard/writer/page.tsx index cb4c7813..cffb5348 100644 --- a/src/app/dashboard/writer/page.tsx +++ b/src/app/dashboard/writer/page.tsx @@ -233,9 +233,12 @@ function WriterDonationHistory({ storylineId }: { storylineId: number }) { className="text-muted flex items-center justify-between text-[10px]" >
- + {truncateAddress(d.donor_address)} - + {d.block_timestamp && (
)} - {/* Farcaster bio (only show for humans without agent description) */} - {!agentMeta?.description && fcProfile?.displayName && fcProfile.displayName !== fcProfile.username && ( -

{fcProfile.displayName}

+ {/* Farcaster bio (only show when no agent description is present) */} + {!agentMeta?.description && fcProfile?.bio && ( +

{fcProfile.bio}

)} diff --git a/src/components/ConnectWallet.tsx b/src/components/ConnectWallet.tsx index 0e8d9cb1..cacf5cd6 100644 --- a/src/components/ConnectWallet.tsx +++ b/src/components/ConnectWallet.tsx @@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from "react"; import { useAccount, useConnect, useDisconnect } from "wagmi"; +import Link from "next/link"; import { isFarcasterMiniApp } from "../../lib/farcaster-detect"; import { truncateAddress } from "../../lib/utils"; import { useConnectedIdentity } from "../hooks/useConnectedIdentity"; @@ -38,7 +39,10 @@ export function ConnectWallet() { if (isConnected && address) { return (
- + {profile?.pfpUrl && ( // eslint-disable-next-line @next/next/no-img-element )} {profile ? `@${profile.username}` : truncateAddress(address)} - +
)} diff --git a/src/components/StoryCard.tsx b/src/components/StoryCard.tsx index f7cca455..d45ddc49 100644 --- a/src/components/StoryCard.tsx +++ b/src/components/StoryCard.tsx @@ -97,11 +97,8 @@ export function StoryCard({ )} - {/* Bottom: author */} -
- - {storyline.writer_type === 1 && } -
+ {/* Bottom spacer (author shown below card with profile link) */} +