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
2 changes: 2 additions & 0 deletions lib/contracts/abi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ export const storylineCreatedEvent = {
{ name: "tokenAddress", type: "address", indexed: false },
{ name: "title", type: "string", indexed: false },
{ name: "hasDeadline", type: "bool", indexed: false },
{ name: "openingCID", type: "string", indexed: false },
{ name: "openingHash", type: "bytes32", indexed: false },
],
} as const;

Expand Down
77 changes: 69 additions & 8 deletions src/app/api/index/storyline/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@ import {
storylineCreatedEvent,
} from "../../../../../lib/contracts/abi";
import { detectWriterType } from "../../../../../lib/contracts/erc8004";
import { hashContent } from "../../../../../lib/content";
import type { Database } from "../../../../../lib/supabase";

const IPFS_GATEWAY = "https://ipfs.filebase.io/ipfs/";
const IPFS_TIMEOUT_MS = 10_000;

/** StorylineCreated event topic0 */
const STORYLINE_CREATED_TOPIC = encodeEventTopics({
abi: [storylineCreatedEvent],
Expand All @@ -22,6 +26,7 @@ function error(message: string, status = 400) {
export async function POST(req: Request) {
const body = await req.json();
const txHash = body.txHash as Hex | undefined;
const fallbackContent = body.content as string | undefined;

if (!txHash || !/^0x[0-9a-fA-F]{64}$/.test(txHash)) {
return error("Missing or invalid txHash");
Expand Down Expand Up @@ -64,8 +69,15 @@ export async function POST(req: Request) {
return error("Unexpected event type");
}

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

// 4. Get block timestamp
let blockTimestamp: bigint;
Expand All @@ -81,34 +93,83 @@ export async function POST(req: Request) {
// 5. Detect writer type via ERC-8004 (best-effort, defaults to human)
const writerType = await detectWriterType(writer);

// 6. Upsert to Supabase
// 6. Fetch genesis plot content from IPFS (with fallback)
let genesisContent: string;
try {
const ipfsRes = await fetch(`${IPFS_GATEWAY}${openingCID}`, {
signal: AbortSignal.timeout(IPFS_TIMEOUT_MS),
});
if (!ipfsRes.ok) throw new Error(`IPFS status ${ipfsRes.status}`);
genesisContent = await ipfsRes.text();
} catch {
if (!fallbackContent) {
return error(
"IPFS fetch failed and no fallback content provided",
502
);
}
genesisContent = fallbackContent;
}

// 7. Verify genesis content hash
const computedHash = hashContent(genesisContent);
if (computedHash !== openingHash) {
return error("Genesis content hash mismatch");
}

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

const row: Database["public"]["Tables"]["storylines"]["Insert"] = {
const timestampISO = new Date(Number(blockTimestamp) * 1000).toISOString();

const storylineRow: 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(),
last_plot_time: timestampISO,
block_timestamp: timestampISO,
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,
storylineRow,
{ onConflict: "tx_hash,log_index" }
);

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

// 9. Insert genesis plot (plot_index = 0) into plots table
const plotRow: Database["public"]["Tables"]["plots"]["Insert"] = {
storyline_id: Number(storylineId),
plot_index: 0,
writer_address: writer.toLowerCase(),
content: genesisContent,
content_cid: openingCID,
content_hash: openingHash as string,
block_timestamp: timestampISO,
tx_hash: txHash.toLowerCase(),
log_index: storylineLog.logIndex!,
};

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

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

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