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
46 changes: 46 additions & 0 deletions lib/contracts/erc8004.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { type Address } from "viem";
import { publicClient } from "../viem";
import { ERC8004_REGISTRY } from "./constants";

/**
* 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.
*/
const erc8004Abi = [
{
type: "function",
name: "agentIdByWallet",
stateMutability: "view",
inputs: [{ name: "wallet", type: "address" }],
outputs: [{ name: "agentId", type: "uint256" }],
},
] as const;

/**
* Check if an address is a registered ERC-8004 agent wallet.
*
* Returns the writer_type value:
* 0 = human (not a registered agent wallet, or query failed)
* 1 = agent (registered agent wallet with agentId > 0)
*
* Best-effort: defaults to 0 (human) on any error.
*/
export async function detectWriterType(
writerAddress: Address
): Promise<number> {
try {
const agentId = await publicClient.readContract({
address: ERC8004_REGISTRY,
abi: erc8004Abi,
functionName: "agentIdByWallet",
args: [writerAddress],
});
return agentId > BigInt(0) ? 1 : 0;
} catch {
// Best-effort: default to human if registry query fails
return 0;
}
}
115 changes: 115 additions & 0 deletions src/app/api/index/storyline/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { NextResponse } from "next/server";
import { type Hex, decodeEventLog, encodeEventTopics } from "viem";
import { publicClient } from "../../../../../lib/viem";
import { createServerClient } from "../../../../../lib/supabase";
import {
storyFactoryAbi,
storylineCreatedEvent,
} from "../../../../../lib/contracts/abi";
import { detectWriterType } from "../../../../../lib/contracts/erc8004";
import type { Database } from "../../../../../lib/supabase";

/** StorylineCreated event topic0 */
const STORYLINE_CREATED_TOPIC = encodeEventTopics({
abi: [storylineCreatedEvent],
eventName: "StorylineCreated",
})[0];

function error(message: string, status = 400) {
return NextResponse.json({ error: message }, { status });
}

export async function POST(req: Request) {
const body = await req.json();
const txHash = body.txHash as Hex | undefined;

if (!txHash || !/^0x[0-9a-fA-F]{64}$/.test(txHash)) {
return error("Missing or invalid txHash");
}

// 1. Fetch receipt
let receipt;
try {
receipt = await publicClient.getTransactionReceipt({ hash: txHash });
} catch {
return error("Failed to fetch transaction receipt", 502);
}

if (receipt.status !== "success") {
return error("Transaction failed");
}

// 2. Find StorylineCreated event log by event signature (topic0)
const storylineLog = receipt.logs.find(
(log) => log.topics[0] === STORYLINE_CREATED_TOPIC
);

if (!storylineLog) {
return error("StorylineCreated event not found in receipt");
}

// 3. Decode event
let decoded;
try {
decoded = decodeEventLog({
abi: storyFactoryAbi,
data: storylineLog.data,
topics: storylineLog.topics,
});
} catch {
return error("Failed to decode StorylineCreated event");
}

if (decoded.eventName !== "StorylineCreated") {
return error("Unexpected event type");
}

const { storylineId, writer, tokenAddress, title, hasDeadline } =
decoded.args;

// 4. Get block timestamp
let blockTimestamp: bigint;
try {
const block = await publicClient.getBlock({
blockNumber: receipt.blockNumber,
});
blockTimestamp = block.timestamp;
} catch {
return error("Failed to fetch block", 502);
}

// 5. Detect writer type via ERC-8004 (best-effort, defaults to human)
const writerType = await detectWriterType(writer);

// 6. Upsert to Supabase
const supabase = createServerClient();
if (!supabase) {
return error("Supabase not configured", 500);
}

const row: Database["public"]["Tables"]["storylines"]["Insert"] = {
storyline_id: Number(storylineId),
writer_address: writer.toLowerCase(),
token_address: tokenAddress.toLowerCase(),
title,
plot_count: 1, // genesis plot
has_deadline: hasDeadline,
writer_type: writerType,
last_plot_time: new Date(Number(blockTimestamp) * 1000).toISOString(),
block_timestamp: new Date(Number(blockTimestamp) * 1000).toISOString(),
tx_hash: txHash.toLowerCase(),
log_index: storylineLog.logIndex!,
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { error: dbError } = await (supabase.from("storylines") as any).upsert(
row,
{ onConflict: "tx_hash,log_index" }
);

if (dbError) {
return error(`Database error: ${dbError.message}`, 500);
}

return NextResponse.json({ success: true });
}
Loading