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
49 changes: 42 additions & 7 deletions lib/contracts/erc8004.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { ERC8004_REGISTRY } from "./constants";
* reverse lookup.
*/
export interface AgentMetadata {
agentId?: string;
owner?: string;
name: string;
description: string;
genre?: string;
Expand Down Expand Up @@ -193,6 +195,29 @@ export async function detectWriterType(
}
}

/**
* Resolve an agent URI string to a parsed JSON object.
* Handles raw JSON, data: URIs (base64 + URL-encoded), https://, and ipfs://.
*/
export async function resolveAgentURI(uri: string): Promise<Record<string, unknown>> {
if (uri.startsWith("{")) {
return JSON.parse(uri);
}
if (uri.startsWith("data:")) {
const comma = uri.indexOf(",");
const payload = comma >= 0 ? uri.slice(comma + 1) : uri;
return JSON.parse(
uri.includes("base64") ? atob(payload) : decodeURIComponent(payload),
);
}
// https:// or ipfs://
const fetchUrl = uri.startsWith("ipfs://")
? uri.replace("ipfs://", "https://ipfs.io/ipfs/")
: uri;
const res = await fetch(fetchUrl);
return (await res.json()) as Record<string, unknown>;
}

/**
* Resolve ERC-8004 agent metadata from an Ethereum address.
* Returns null if the address is not a registered agent or on any error.
Expand All @@ -209,16 +234,26 @@ export async function getAgentMetadata(
});
if (agentId <= BigInt(0)) return null;

const uri = await publicClient.readContract({
address: ERC8004_REGISTRY,
abi: erc8004Abi,
functionName: "agentURI",
args: [agentId],
});
const [uri, owner] = await Promise.all([
publicClient.readContract({
address: ERC8004_REGISTRY,
abi: erc8004Abi,
functionName: "agentURI",
args: [agentId],
}),
publicClient.readContract({
address: ERC8004_REGISTRY,
abi: erc8004Abi,
functionName: "ownerOf",
args: [agentId],
}).catch(() => undefined),
]);
if (!uri) return null;

const parsed = JSON.parse(uri as string) as Record<string, unknown>;
const parsed = await resolveAgentURI(uri as string);
return {
agentId: agentId.toString(),
owner: owner as string | undefined,
name: (parsed.name as string) || "Unknown Agent",
description: (parsed.description as string) || "",
genre: (parsed.genre as string) || undefined,
Expand Down
53 changes: 53 additions & 0 deletions src/app/profile/[address]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { formatUnits, type Address } from "viem";
import Link from "next/link";
import { supabase, type Storyline, type Donation, type TradeHistory, type User } from "../../../../lib/supabase";

Check warning on line 9 in src/app/profile/[address]/page.tsx

View workflow job for this annotation

GitHub Actions / lint-and-typecheck

'TradeHistory' is defined but never used
import { STORY_FACTORY, RESERVE_LABEL, EXPLORER_URL, MCV2_BOND, PLOT_TOKEN } from "../../../../lib/contracts/constants";
import { getFarcasterProfile, fetchAgentMetadata, getUserFromDB } from "../../../../lib/actions";
import { truncateAddress } from "../../../../lib/utils";
import { formatPrice } from "../../../../lib/format";
import { getTokenPrice, mcv2BondAbi, erc20Abi, type TokenPriceInfo } from "../../../../lib/price";

Check warning on line 14 in src/app/profile/[address]/page.tsx

View workflow job for this annotation

GitHub Actions / lint-and-typecheck

'TokenPriceInfo' is defined but never used
import { browserClient } from "../../../../lib/rpc";
import type { FarcasterProfile } from "../../../../lib/farcaster";
import type { AgentMetadata } from "../../../../lib/contracts/erc8004";
Expand Down Expand Up @@ -114,7 +114,7 @@
if (r <= 0) clearInterval(interval);
}, 1000);
return () => clearInterval(interval);
}, [dbUser?.steemhunt_fetched_at]);

Check warning on line 117 in src/app/profile/[address]/page.tsx

View workflow job for this annotation

GitHub Actions / lint-and-typecheck

React Hook useEffect has a missing dependency: 'COOLDOWN_MS'. Either include it or remove the dependency array

const onCooldown = cooldownRemaining > 0;

Expand Down Expand Up @@ -372,6 +372,59 @@
</div>
)}

{/* Agent Identity card — shown for registered agents */}
{isAgent && agentMeta && (
<div className="border-border rounded border p-3">
<span className="text-muted text-[10px] font-medium uppercase tracking-wider">Agent Identity</span>
<div className="mt-1.5 space-y-1.5">
{agentMeta.agentId && (
<div className="text-xs">
<span className="text-muted">Agent ID: </span>
<span className="text-foreground font-mono font-medium">{agentMeta.agentId}</span>
</div>
)}
<div className="text-xs">
<span className="text-muted">Name: </span>
<span className="text-foreground font-medium">{agentMeta.name}</span>
</div>
{agentMeta.description && (
<div className="text-xs">
<span className="text-muted">Description: </span>
<span className="text-foreground">{agentMeta.description}</span>
</div>
)}
{agentMeta.llmModel && (
<div className="text-xs">
<span className="text-muted">Model: </span>
<span className="text-foreground font-medium">{agentMeta.llmModel}</span>
</div>
)}
{agentMeta.genre && (
<div className="text-xs">
<span className="text-muted">Genre: </span>
<span className="text-foreground">{agentMeta.genre}</span>
</div>
)}
{agentMeta.registeredAt && (
<div className="text-xs">
<span className="text-muted">Registered: </span>
<span className="text-foreground">
{new Date(agentMeta.registeredAt).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })}
</span>
</div>
)}
{agentMeta.owner && agentMeta.owner.toLowerCase() !== address.toLowerCase() && (
<div className="text-xs">
<span className="text-muted">Owner: </span>
<Link href={`/profile/${agentMeta.owner}`} className="text-accent hover:underline font-mono text-[11px]">
{agentMeta.owner.slice(0, 6)}...{agentMeta.owner.slice(-4)}
</Link>
</div>
)}
</div>
</div>
)}

{/* Wallet identity card — always shown */}
<div className="border-border rounded border p-3">
<span className="text-muted text-[10px] font-medium uppercase tracking-wider">Wallet</span>
Expand Down
23 changes: 2 additions & 21 deletions src/components/AgentManage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useState, useEffect, useMemo } from "react";
import { useAccount, useWriteContract, useReadContract, useSignTypedData } from "wagmi";
import { type Hex, type Address, zeroAddress } from "viem";
import { browserClient as publicClient } from "../../lib/rpc";
import { erc8004Abi, type AgentMetadata } from "../../lib/contracts/erc8004";
import { erc8004Abi, resolveAgentURI, type AgentMetadata } from "../../lib/contracts/erc8004";
import { ERC8004_REGISTRY, BASE_CHAIN_ID, EXPLORER_URL } from "../../lib/contracts/constants";
import { Select } from "./Select";

Expand Down Expand Up @@ -98,26 +98,7 @@ export function AgentManage({ agentId, role }: AgentManageProps) {
});
if (cancelled) return;
if (!uri) { setMetadata(null); return; }
const uriStr = uri as string;
let parsed: Record<string, unknown>;
if (uriStr.startsWith("{")) {
// Raw JSON stored directly in the URI field
parsed = JSON.parse(uriStr);
} else if (uriStr.startsWith("data:")) {
// data: URI — extract the base64/json payload
const comma = uriStr.indexOf(",");
const payload = comma >= 0 ? uriStr.slice(comma + 1) : uriStr;
parsed = JSON.parse(
uriStr.includes("base64") ? atob(payload) : decodeURIComponent(payload),
);
} else {
// https:// or ipfs:// — fetch the metadata JSON
const fetchUrl = uriStr.startsWith("ipfs://")
? uriStr.replace("ipfs://", "https://ipfs.io/ipfs/")
: uriStr;
const res = await fetch(fetchUrl);
parsed = (await res.json()) as Record<string, unknown>;
}
const parsed = await resolveAgentURI(uri as string);
setMetadata({
name: (parsed.name as string) || "Unknown Agent",
description: (parsed.description as string) || "",
Expand Down
Loading