From e463210f2fd692ab58762eeab08fb20785170750 Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Tue, 17 Mar 2026 08:19:53 +0000 Subject: [PATCH 1/3] [#233] Add contract_address column for multi-contract support Migration: - Added contract_address TEXT NOT NULL to all 6 tables - Existing rows default to old contract (0x05c4...b474) - Default changed to '' after backfill (indexers pass explicitly) TypeScript types: - Added contract_address to all Row/Insert/Update types Indexer routes (stamp on insert): - storyline, plot, donation indexers: contract_address in upsert rows - views API: contract_address on page_views insert - comments API: contract_address on comment insert - ratings API: contract_address on ratings upsert Frontend queries (filter by STORY_FACTORY): - Home page, story page, plot detail, OG route - Writer dashboard, reader dashboard, chain page - Ranking (trending/rising), ReaderPortfolio - All API GET handlers All queries use STORY_FACTORY.toLowerCase() from constants.ts. Switching NEXT_PUBLIC_CONTRACT_ADDRESS instantly shows only new contract data while preserving old data. Fixes #233 Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/ranking.ts | 11 +++++++++-- lib/supabase.ts | 18 ++++++++++++++++++ src/app/api/comments/route.ts | 7 ++++++- src/app/api/index/donation/route.ts | 2 ++ src/app/api/index/plot/route.ts | 2 ++ src/app/api/index/storyline/route.ts | 3 +++ src/app/api/ratings/route.ts | 8 +++++++- src/app/api/views/route.ts | 3 +++ src/app/chain/page.tsx | 2 ++ src/app/dashboard/reader/page.tsx | 3 ++- src/app/dashboard/writer/page.tsx | 4 +++- src/app/page.tsx | 10 +++++++--- .../story/[storylineId]/[plotIndex]/page.tsx | 11 ++++++----- src/app/story/[storylineId]/og/route.tsx | 3 ++- src/app/story/[storylineId]/page.tsx | 5 ++++- src/components/ReaderPortfolio.tsx | 3 ++- supabase/migrations/00009_contract_address.sql | 18 ++++++++++++++++++ 17 files changed, 96 insertions(+), 17 deletions(-) create mode 100644 supabase/migrations/00009_contract_address.sql diff --git a/lib/ranking.ts b/lib/ranking.ts index 5a8a6079..4816a5cf 100644 --- a/lib/ranking.ts +++ b/lib/ranking.ts @@ -1,5 +1,6 @@ import { type Address, formatUnits } from "viem"; import { get24hPriceChange, getTokenTVL } from "./price"; +import { STORY_FACTORY } from "./contracts/constants"; import type { Storyline } from "./supabase"; import type { SupabaseClient } from "@supabase/supabase-js"; @@ -62,7 +63,8 @@ async function fetchCandidatesAndRatings(supabase: SupabaseClient, writerType?: .select("*") .eq("hidden", false) .eq("sunset", false) - .neq("token_address", ""); + .neq("token_address", "") + .eq("contract_address", STORY_FACTORY.toLowerCase()); if (writerType !== undefined) q = q.eq("writer_type", writerType); const { data } = await q .order("block_timestamp", { ascending: false }) @@ -76,7 +78,8 @@ async function fetchCandidatesAndRatings(supabase: SupabaseClient, writerType?: // eslint-disable-next-line @typescript-eslint/no-explicit-any const { data: allRatings } = await (supabase.from("ratings") as any) .select("storyline_id, rating") - .in("storyline_id", storylineIds); + .in("storyline_id", storylineIds) + .eq("contract_address", STORY_FACTORY.toLowerCase()); const ratingMap = new Map(); if (allRatings) { @@ -182,12 +185,14 @@ export async function getRisingStorylines( const { data: recentRatings } = await (supabase.from("ratings") as any) .select("storyline_id, rating") .in("storyline_id", storylineIds) + .eq("contract_address", STORY_FACTORY.toLowerCase()) .gte("updated_at", threeDaysAgo); // eslint-disable-next-line @typescript-eslint/no-explicit-any const { data: priorRatings } = await (supabase.from("ratings") as any) .select("storyline_id, rating") .in("storyline_id", storylineIds) + .eq("contract_address", STORY_FACTORY.toLowerCase()) .gte("updated_at", sixDaysAgo) .lt("updated_at", threeDaysAgo); @@ -196,12 +201,14 @@ export async function getRisingStorylines( const { data: recentPlots } = await (supabase.from("plots") as any) .select("storyline_id") .in("storyline_id", storylineIds) + .eq("contract_address", STORY_FACTORY.toLowerCase()) .gte("block_timestamp", threeDaysAgo); // eslint-disable-next-line @typescript-eslint/no-explicit-any const { data: priorPlots } = await (supabase.from("plots") as any) .select("storyline_id") .in("storyline_id", storylineIds) + .eq("contract_address", STORY_FACTORY.toLowerCase()) .gte("block_timestamp", sixDaysAgo) .lt("block_timestamp", threeDaysAgo); diff --git a/lib/supabase.ts b/lib/supabase.ts index bbd58282..de046a18 100644 --- a/lib/supabase.ts +++ b/lib/supabase.ts @@ -61,6 +61,7 @@ export interface Database { block_timestamp: string | null; indexed_at: string; view_count: number; + contract_address: string; }; Insert: { id?: never; @@ -79,6 +80,7 @@ export interface Database { block_timestamp?: string | null; indexed_at?: string; view_count?: number; + contract_address?: string; }; Update: { id?: never; @@ -97,6 +99,7 @@ export interface Database { block_timestamp?: string | null; indexed_at?: string; view_count?: number; + contract_address?: string; }; }; page_views: { @@ -107,6 +110,7 @@ export interface Database { viewer_address: string | null; session_id: string; viewed_at: string; + contract_address: string; }; Insert: { id?: never; @@ -115,6 +119,7 @@ export interface Database { viewer_address?: string | null; session_id: string; viewed_at?: string; + contract_address?: string; }; Update: { id?: never; @@ -123,6 +128,7 @@ export interface Database { viewer_address?: string | null; session_id?: string; viewed_at?: string; + contract_address?: string; }; }; plots: { @@ -140,6 +146,7 @@ export interface Database { log_index: number; block_timestamp: string | null; indexed_at: string; + contract_address: string; }; Insert: { id?: never; @@ -155,6 +162,7 @@ export interface Database { log_index: number; block_timestamp?: string | null; indexed_at?: string; + contract_address?: string; }; Update: { id?: never; @@ -170,6 +178,7 @@ export interface Database { log_index?: number; block_timestamp?: string | null; indexed_at?: string; + contract_address?: string; }; }; comments: { @@ -181,6 +190,7 @@ export interface Database { content: string; created_at: string; hidden: boolean; + contract_address: string; }; Insert: { id?: never; @@ -190,6 +200,7 @@ export interface Database { content: string; created_at?: string; hidden?: boolean; + contract_address?: string; }; Update: { id?: never; @@ -199,6 +210,7 @@ export interface Database { content?: string; created_at?: string; hidden?: boolean; + contract_address?: string; }; }; donations: { @@ -211,6 +223,7 @@ export interface Database { log_index: number; block_timestamp: string | null; indexed_at: string; + contract_address: string; }; Insert: { id?: never; @@ -221,6 +234,7 @@ export interface Database { log_index: number; block_timestamp?: string | null; indexed_at?: string; + contract_address?: string; }; Update: { id?: never; @@ -231,6 +245,7 @@ export interface Database { log_index?: number; block_timestamp?: string | null; indexed_at?: string; + contract_address?: string; }; }; ratings: { @@ -242,6 +257,7 @@ export interface Database { comment: string | null; created_at: string; updated_at: string; + contract_address: string; }; Insert: { id?: never; @@ -251,6 +267,7 @@ export interface Database { comment?: string | null; created_at?: string; updated_at?: string; + contract_address?: string; }; Update: { id?: never; @@ -260,6 +277,7 @@ export interface Database { comment?: string | null; created_at?: string; updated_at?: string; + contract_address?: string; }; }; }; diff --git a/src/app/api/comments/route.ts b/src/app/api/comments/route.ts index 1dae1157..29a3b40a 100644 --- a/src/app/api/comments/route.ts +++ b/src/app/api/comments/route.ts @@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from "next/server"; import { type Address } from "viem"; import { publicClient } from "../../../../lib/rpc"; import { createServerClient, supabase } from "../../../../lib/supabase"; +import { STORY_FACTORY } from "../../../../lib/contracts/constants"; const MAX_COMMENT_LENGTH = 1000; const DEFAULT_LIMIT = 20; @@ -38,6 +39,7 @@ export async function GET(req: NextRequest) { .eq("storyline_id", sid) .eq("plot_index", pidx) .eq("hidden", false) + .eq("contract_address", STORY_FACTORY.toLowerCase()) .order("created_at", { ascending: false }) .range(offset, offset + limit - 1); @@ -49,7 +51,8 @@ export async function GET(req: NextRequest) { .select("id", { count: "exact", head: true }) .eq("storyline_id", sid) .eq("plot_index", pidx) - .eq("hidden", false); + .eq("hidden", false) + .eq("contract_address", STORY_FACTORY.toLowerCase()); return NextResponse.json({ comments: data ?? [], @@ -120,6 +123,7 @@ export async function POST(req: NextRequest) { .eq("storyline_id", storylineId) .eq("plot_index", plotIndex) .eq("commenter_address", commenterAddress.toLowerCase()) + .eq("contract_address", STORY_FACTORY.toLowerCase()) .gte("created_at", oneMinuteAgo) .limit(1); @@ -137,6 +141,7 @@ export async function POST(req: NextRequest) { plot_index: plotIndex, commenter_address: commenterAddress.toLowerCase(), content, + contract_address: STORY_FACTORY.toLowerCase(), }); if (insertError) return error(`Database error: ${insertError.message}`, 500); diff --git a/src/app/api/index/donation/route.ts b/src/app/api/index/donation/route.ts index 415b73d1..49ba03ee 100644 --- a/src/app/api/index/donation/route.ts +++ b/src/app/api/index/donation/route.ts @@ -6,6 +6,7 @@ import { storyFactoryAbi, donationEvent, } from "../../../../../lib/contracts/abi"; +import { STORY_FACTORY } from "../../../../../lib/contracts/constants"; import type { Database } from "../../../../../lib/supabase"; /** Donation event topic0 */ @@ -89,6 +90,7 @@ export async function POST(req: Request) { block_timestamp: new Date(Number(blockTimestamp) * 1000).toISOString(), tx_hash: txHash.toLowerCase(), log_index: donationLog.logIndex!, + contract_address: STORY_FACTORY.toLowerCase(), }; // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/src/app/api/index/plot/route.ts b/src/app/api/index/plot/route.ts index 9acdb7a2..74f86d1e 100644 --- a/src/app/api/index/plot/route.ts +++ b/src/app/api/index/plot/route.ts @@ -6,6 +6,7 @@ import { storyFactoryAbi, plotChainedEvent, } from "../../../../../lib/contracts/abi"; +import { STORY_FACTORY } from "../../../../../lib/contracts/constants"; import { hashContent } from "../../../../../lib/content"; import type { Database } from "../../../../../lib/supabase"; @@ -120,6 +121,7 @@ export async function POST(req: Request) { block_timestamp: new Date(Number(blockTimestamp) * 1000).toISOString(), tx_hash: txHash.toLowerCase(), log_index: plotChainedLog.logIndex!, + contract_address: STORY_FACTORY.toLowerCase(), }; // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/src/app/api/index/storyline/route.ts b/src/app/api/index/storyline/route.ts index 9bcc3b4d..e1de2a72 100644 --- a/src/app/api/index/storyline/route.ts +++ b/src/app/api/index/storyline/route.ts @@ -6,6 +6,7 @@ import { storyFactoryAbi, storylineCreatedEvent, } from "../../../../../lib/contracts/abi"; +import { STORY_FACTORY } from "../../../../../lib/contracts/constants"; import { detectWriterType } from "../../../../../lib/contracts/erc8004"; import { hashContent } from "../../../../../lib/content"; import type { Database } from "../../../../../lib/supabase"; @@ -137,6 +138,7 @@ export async function POST(req: Request) { block_timestamp: timestampISO, tx_hash: txHash.toLowerCase(), log_index: storylineLog.logIndex!, + contract_address: STORY_FACTORY.toLowerCase(), }; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -160,6 +162,7 @@ export async function POST(req: Request) { block_timestamp: timestampISO, tx_hash: txHash.toLowerCase(), log_index: storylineLog.logIndex!, + contract_address: STORY_FACTORY.toLowerCase(), }; // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/src/app/api/ratings/route.ts b/src/app/api/ratings/route.ts index e328cbe8..1d2fc02d 100644 --- a/src/app/api/ratings/route.ts +++ b/src/app/api/ratings/route.ts @@ -3,6 +3,7 @@ import { type Address } from "viem"; import { publicClient } from "../../../../lib/rpc"; import { createServerClient, supabase } from "../../../../lib/supabase"; import { erc20Abi } from "../../../../lib/price"; +import { STORY_FACTORY } from "../../../../lib/contracts/constants"; const MAX_COMMENT_LENGTH = 500; @@ -33,7 +34,8 @@ export async function GET(req: NextRequest) { // eslint-disable-next-line @typescript-eslint/no-explicit-any const { data: allData, error: allError } = await (db.from("ratings") as any) .select("rating") - .eq("storyline_id", sid); + .eq("storyline_id", sid) + .eq("contract_address", STORY_FACTORY.toLowerCase()); if (allError) { return error(`Database error: ${allError.message}`, 500); @@ -51,6 +53,7 @@ export async function GET(req: NextRequest) { const { data, error: dbError } = await (db.from("ratings") as any) .select("*") .eq("storyline_id", sid) + .eq("contract_address", STORY_FACTORY.toLowerCase()) .order("updated_at", { ascending: false }) .range(offset, offset + limit - 1); @@ -69,6 +72,7 @@ export async function GET(req: NextRequest) { .select("*") .eq("storyline_id", sid) .eq("rater_address", raterAddress.toLowerCase()) + .eq("contract_address", STORY_FACTORY.toLowerCase()) .single(); myRating = mine ?? null; } @@ -147,6 +151,7 @@ export async function POST(req: NextRequest) { const { data: storyline, error: slError } = await (serverClient.from("storylines") as any) .select("token_address") .eq("storyline_id", storylineId) + .eq("contract_address", STORY_FACTORY.toLowerCase()) .single(); if (slError || !storyline) { @@ -180,6 +185,7 @@ export async function POST(req: NextRequest) { rating, comment: comment ?? null, updated_at: new Date().toISOString(), + contract_address: STORY_FACTORY.toLowerCase(), }, { onConflict: "storyline_id,rater_address" }, ); diff --git a/src/app/api/views/route.ts b/src/app/api/views/route.ts index 124e9199..e57923de 100644 --- a/src/app/api/views/route.ts +++ b/src/app/api/views/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { createServerClient, supabase } from "../../../../lib/supabase"; +import { STORY_FACTORY } from "../../../../lib/contracts/constants"; function error(message: string, status = 400) { return NextResponse.json({ error: message }, { status }); @@ -59,6 +60,7 @@ export async function GET(req: NextRequest) { const { data, error: dbError } = await (db.from("storylines") as any) .select("view_count") .eq("storyline_id", sid) + .eq("contract_address", STORY_FACTORY.toLowerCase()) .single(); if (dbError) return error(`Database error: ${dbError.message}`, 500); @@ -141,6 +143,7 @@ export async function POST(req: NextRequest) { plot_index: plotVal, viewer_address: viewerAddress?.toLowerCase() ?? null, session_id: sessionId, + contract_address: STORY_FACTORY.toLowerCase(), }); if (insertError) return error(`Database error: ${insertError.message}`, 500); diff --git a/src/app/chain/page.tsx b/src/app/chain/page.tsx index 3737958c..7355797f 100644 --- a/src/app/chain/page.tsx +++ b/src/app/chain/page.tsx @@ -9,6 +9,7 @@ import { MAX_CONTENT_LENGTH, } from "../../../lib/content"; import { supabase, type Storyline } from "../../../lib/supabase"; +import { STORY_FACTORY } from "../../../lib/contracts/constants"; import { useChainPlot } from "../../hooks/useChainPlot"; import type { PublishState } from "../../hooks/usePublish"; import Link from "next/link"; @@ -33,6 +34,7 @@ async function fetchWriterStorylines(address: string): Promise { .eq("writer_address", address.toLowerCase()) .eq("hidden", false) .eq("sunset", false) + .eq("contract_address", STORY_FACTORY.toLowerCase()) .order("block_timestamp", { ascending: false }) .returns(); return data ?? []; diff --git a/src/app/dashboard/reader/page.tsx b/src/app/dashboard/reader/page.tsx index c6722ed1..30f80326 100644 --- a/src/app/dashboard/reader/page.tsx +++ b/src/app/dashboard/reader/page.tsx @@ -8,7 +8,7 @@ import { ReaderPortfolio } from "../../../components/ReaderPortfolio"; import { WriterIdentityClient } from "../../../components/WriterIdentityClient"; import { formatUnits } from "viem"; import { ConnectWallet } from "../../../components/ConnectWallet"; -import { RESERVE_LABEL, PLOT_TOKEN } from "../../../../lib/contracts/constants"; +import { RESERVE_LABEL, PLOT_TOKEN, STORY_FACTORY } from "../../../../lib/contracts/constants"; import { publicClient } from "../../../../lib/rpc"; import { type Address } from "viem"; @@ -38,6 +38,7 @@ async function fetchDonationPage( .from("donations") .select("*", { count: "exact" }) .eq("donor_address", address.toLowerCase()) + .eq("contract_address", STORY_FACTORY.toLowerCase()) .order("block_timestamp", { ascending: false }) .range(from, to) .returns(); diff --git a/src/app/dashboard/writer/page.tsx b/src/app/dashboard/writer/page.tsx index 63443d9d..4f88bae3 100644 --- a/src/app/dashboard/writer/page.tsx +++ b/src/app/dashboard/writer/page.tsx @@ -6,7 +6,7 @@ import { useQuery } from "@tanstack/react-query"; import { formatUnits } from "viem"; import { supabase, type Storyline } from "../../../../lib/supabase"; import { getTokenTVL } from "../../../../lib/price"; -import { RESERVE_LABEL } from "../../../../lib/contracts/constants"; +import { RESERVE_LABEL, STORY_FACTORY } from "../../../../lib/contracts/constants"; import { DeadlineCountdown } from "../../../components/DeadlineCountdown"; import { ClaimRoyalties } from "../../../components/ClaimRoyalties"; import { WriterTradingStats } from "../../../components/WriterTradingStats"; @@ -31,6 +31,7 @@ async function fetchWriterStorylines( .select("*") .eq("writer_address", address.toLowerCase()) .eq("hidden", false) + .eq("contract_address", STORY_FACTORY.toLowerCase()) .order("block_timestamp", { ascending: false }) .returns(); if (error) throw error; @@ -176,6 +177,7 @@ function DonationCount({ storylineId, tokenAddress }: { storylineId: number; tok (supabase.from("donations") as any) .select("amount") .eq("storyline_id", storylineId) + .eq("contract_address", STORY_FACTORY.toLowerCase()) .then((r: { data: { amount: string }[] | null }) => r.data) : null, ]); diff --git a/src/app/page.tsx b/src/app/page.tsx index b4012006..4443e55b 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,4 +1,5 @@ import { createServerClient, type Storyline } from "../../lib/supabase"; +import { STORY_FACTORY } from "../../lib/contracts/constants"; import { getTrendingStorylines, getRisingStorylines } from "../../lib/ranking"; import { StoryCard } from "../components/StoryCard"; import { SortDropdown } from "../components/SortDropdown"; @@ -42,7 +43,8 @@ export default async function Home({ const { data: plots } = await (supabase.from("plots") as any) .select("storyline_id, content") .in("storyline_id", storylines.map((s) => s.storyline_id)) - .eq("plot_index", 0); + .eq("plot_index", 0) + .eq("contract_address", STORY_FACTORY.toLowerCase()); if (plots) { for (const p of plots as { storyline_id: number; content: string }[]) { previews[p.storyline_id] = p.content.slice(0, 120); @@ -143,7 +145,8 @@ async function queryTab( .from("storylines") .select("*") .eq("hidden", false) - .eq("sunset", false); + .eq("sunset", false) + .eq("contract_address", STORY_FACTORY.toLowerCase()); if (writer === "human") q = q.eq("writer_type", 0); if (writer === "agent") q = q.eq("writer_type", 1); const { data } = await q @@ -158,7 +161,8 @@ async function queryTab( .from("storylines") .select("*") .eq("hidden", false) - .eq("sunset", true); + .eq("sunset", true) + .eq("contract_address", STORY_FACTORY.toLowerCase()); if (writer === "human") q = q.eq("writer_type", 0); if (writer === "agent") q = q.eq("writer_type", 1); const { data } = await q diff --git a/src/app/story/[storylineId]/[plotIndex]/page.tsx b/src/app/story/[storylineId]/[plotIndex]/page.tsx index 8329d024..908c413e 100644 --- a/src/app/story/[storylineId]/[plotIndex]/page.tsx +++ b/src/app/story/[storylineId]/[plotIndex]/page.tsx @@ -1,6 +1,7 @@ import { type Metadata } from "next"; import { redirect } from "next/navigation"; import { createServerClient, type Storyline, type Plot } from "../../../../../lib/supabase"; +import { STORY_FACTORY } from "../../../../../lib/contracts/constants"; import { truncateAddress } from "../../../../../lib/utils"; import { ViewTracker } from "../../../../components/ViewCount"; import { CommentSection } from "../../../../components/CommentSection"; @@ -27,8 +28,8 @@ export async function generateMetadata({ if (!supabase) return {}; const [{ data: storyline }, { data: plot }] = await Promise.all([ - supabase.from("storylines").select("*").eq("storyline_id", sid).eq("hidden", false).single(), - supabase.from("plots").select("*").eq("storyline_id", sid).eq("plot_index", pidx).eq("hidden", false).single(), + supabase.from("storylines").select("*").eq("storyline_id", sid).eq("hidden", false).eq("contract_address", STORY_FACTORY.toLowerCase()).single(), + supabase.from("plots").select("*").eq("storyline_id", sid).eq("plot_index", pidx).eq("hidden", false).eq("contract_address", STORY_FACTORY.toLowerCase()).single(), ]); if (!storyline || !plot) return {}; @@ -67,9 +68,9 @@ export default async function PlotDetailPage({ params }: { params: Params }) { if (!supabase) return ; const [{ data: storyline }, { data: plot }, { data: plotRows }] = await Promise.all([ - supabase.from("storylines").select("*").eq("storyline_id", sid).eq("hidden", false).single(), - supabase.from("plots").select("*").eq("storyline_id", sid).eq("plot_index", pidx).eq("hidden", false).single(), - supabase.from("plots").select("plot_index").eq("storyline_id", sid).eq("hidden", false).order("plot_index", { ascending: true }), + supabase.from("storylines").select("*").eq("storyline_id", sid).eq("hidden", false).eq("contract_address", STORY_FACTORY.toLowerCase()).single(), + supabase.from("plots").select("*").eq("storyline_id", sid).eq("plot_index", pidx).eq("hidden", false).eq("contract_address", STORY_FACTORY.toLowerCase()).single(), + supabase.from("plots").select("plot_index").eq("storyline_id", sid).eq("hidden", false).eq("contract_address", STORY_FACTORY.toLowerCase()).order("plot_index", { ascending: true }), ]); if (!storyline) return ; diff --git a/src/app/story/[storylineId]/og/route.tsx b/src/app/story/[storylineId]/og/route.tsx index 254950b0..da5c5e61 100644 --- a/src/app/story/[storylineId]/og/route.tsx +++ b/src/app/story/[storylineId]/og/route.tsx @@ -2,7 +2,7 @@ import { ImageResponse } from "next/og"; import { type Address } from "viem"; import { createServerClient, type Storyline } from "../../../../../lib/supabase"; import { getTokenPrice } from "../../../../../lib/price"; -import { RESERVE_LABEL } from "../../../../../lib/contracts/constants"; +import { RESERVE_LABEL, STORY_FACTORY } from "../../../../../lib/contracts/constants"; import { truncateAddress } from "../../../../../lib/utils"; export const runtime = "edge"; @@ -28,6 +28,7 @@ export async function GET( .select("*") .eq("storyline_id", id) .eq("hidden", false) + .eq("contract_address", STORY_FACTORY.toLowerCase()) .single(); if (!storyline) { diff --git a/src/app/story/[storylineId]/page.tsx b/src/app/story/[storylineId]/page.tsx index bbcda4d6..7c862592 100644 --- a/src/app/story/[storylineId]/page.tsx +++ b/src/app/story/[storylineId]/page.tsx @@ -9,7 +9,7 @@ import { RatingWidget } from "../../../components/RatingWidget"; import { RatingSummary } from "../../../components/RatingSummary"; import { ShareToFarcaster } from "../../../components/ShareToFarcaster"; import { getTokenPrice, type TokenPriceInfo } from "../../../../lib/price"; -import { RESERVE_LABEL } from "../../../../lib/contracts/constants"; +import { RESERVE_LABEL, STORY_FACTORY } from "../../../../lib/contracts/constants"; import { type Address } from "viem"; import { truncateAddress } from "../../../../lib/utils"; import Link from "next/link"; @@ -40,6 +40,7 @@ export async function generateMetadata({ .select("*") .eq("storyline_id", id) .eq("hidden", false) + .eq("contract_address", STORY_FACTORY.toLowerCase()) .single(); if (!storyline) return {}; @@ -103,6 +104,7 @@ export default async function StoryPage({ params }: { params: Params }) { .select("*") .eq("storyline_id", id) .eq("hidden", false) + .eq("contract_address", STORY_FACTORY.toLowerCase()) .single(); if (!storyline) { @@ -114,6 +116,7 @@ export default async function StoryPage({ params }: { params: Params }) { .select("*") .eq("storyline_id", id) .eq("hidden", false) + .eq("contract_address", STORY_FACTORY.toLowerCase()) .order("plot_index", { ascending: true }) .returns(); diff --git a/src/components/ReaderPortfolio.tsx b/src/components/ReaderPortfolio.tsx index 88406739..003a7d46 100644 --- a/src/components/ReaderPortfolio.tsx +++ b/src/components/ReaderPortfolio.tsx @@ -5,7 +5,7 @@ import { useQuery } from "@tanstack/react-query"; import { formatUnits, type Address } from "viem"; import { publicClient } from "../../lib/rpc"; import { erc20Abi, mcv2BondAbi, get24hPriceChange, getTokenTVL } from "../../lib/price"; -import { MCV2_BOND, IS_TESTNET } from "../../lib/contracts/constants"; +import { MCV2_BOND, IS_TESTNET, STORY_FACTORY } from "../../lib/contracts/constants"; import { supabase, type Storyline } from "../../lib/supabase"; import Link from "next/link"; @@ -33,6 +33,7 @@ export function ReaderPortfolio() { .select("*") .eq("hidden", false) .neq("token_address", "") + .eq("contract_address", STORY_FACTORY.toLowerCase()) .returns(); if (!storylines || storylines.length === 0) return []; diff --git a/supabase/migrations/00009_contract_address.sql b/supabase/migrations/00009_contract_address.sql new file mode 100644 index 00000000..c6585571 --- /dev/null +++ b/supabase/migrations/00009_contract_address.sql @@ -0,0 +1,18 @@ +-- Add contract_address to all tables for multi-contract support +-- Default existing rows to old contract address (lowercase) + +ALTER TABLE storylines ADD COLUMN contract_address TEXT NOT NULL DEFAULT '0x05c4d59529807316d6fa09cdaa509addfe85b474'; +ALTER TABLE plots ADD COLUMN contract_address TEXT NOT NULL DEFAULT '0x05c4d59529807316d6fa09cdaa509addfe85b474'; +ALTER TABLE donations ADD COLUMN contract_address TEXT NOT NULL DEFAULT '0x05c4d59529807316d6fa09cdaa509addfe85b474'; +ALTER TABLE ratings ADD COLUMN contract_address TEXT NOT NULL DEFAULT '0x05c4d59529807316d6fa09cdaa509addfe85b474'; +ALTER TABLE comments ADD COLUMN contract_address TEXT NOT NULL DEFAULT '0x05c4d59529807316d6fa09cdaa509addfe85b474'; +ALTER TABLE page_views ADD COLUMN contract_address TEXT NOT NULL DEFAULT '0x05c4d59529807316d6fa09cdaa509addfe85b474'; + +-- After backfilling existing rows, change default to empty string +-- Indexers will always explicitly pass the value +ALTER TABLE storylines ALTER COLUMN contract_address SET DEFAULT ''; +ALTER TABLE plots ALTER COLUMN contract_address SET DEFAULT ''; +ALTER TABLE donations ALTER COLUMN contract_address SET DEFAULT ''; +ALTER TABLE ratings ALTER COLUMN contract_address SET DEFAULT ''; +ALTER TABLE comments ALTER COLUMN contract_address SET DEFAULT ''; +ALTER TABLE page_views ALTER COLUMN contract_address SET DEFAULT ''; From 6efbb143ec9e3eecfdf5c397f57ba14f7fa41ae3 Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Tue, 17 Mar 2026 08:22:29 +0000 Subject: [PATCH 2/3] [#233] Add contract_address to backfill cron inserts Stamps contract_address: STORY_FACTORY.toLowerCase() on all 4 insert objects in the backfill route (storyline, genesis plot, chained plot, donation). Addresses T2a review feedback on PR #234. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app/api/cron/backfill/route.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/app/api/cron/backfill/route.ts b/src/app/api/cron/backfill/route.ts index b4717ea2..89a89bc9 100644 --- a/src/app/api/cron/backfill/route.ts +++ b/src/app/api/cron/backfill/route.ts @@ -219,6 +219,7 @@ async function processStorylineCreated( block_timestamp: timestampISO, tx_hash: txHash, log_index: logIndex, + contract_address: STORY_FACTORY.toLowerCase(), }; const { error: storylineError } = await supabase @@ -241,6 +242,7 @@ async function processStorylineCreated( block_timestamp: timestampISO, tx_hash: txHash, log_index: logIndex, + contract_address: STORY_FACTORY.toLowerCase(), }; const { error: plotError } = await supabase .from("plots") @@ -278,6 +280,7 @@ async function processPlotChained( block_timestamp: timestampISO, tx_hash: txHash, log_index: logIndex, + contract_address: STORY_FACTORY.toLowerCase(), }; const { error: plotError } = await supabase @@ -306,6 +309,7 @@ async function processDonation( block_timestamp: timestampISO, tx_hash: txHash, log_index: logIndex, + contract_address: STORY_FACTORY.toLowerCase(), }; const { error: donationError } = await supabase From 9aab66752bd54ad93882461540d5380d07bd4de5 Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Tue, 17 Mar 2026 08:34:50 +0000 Subject: [PATCH 3/3] [#233] Fix T2b review bugs: migration default, ratings upsert, view dedup, RPC scope - Bug 1: Fix migration default to match current STORY_FACTORY address - Bug 2: Add contract_address to ratings upsert onConflict - Bug 3: Add contract_address filter to view dedup query - Bug 4: Scope increment_view_count RPC to contract_address - Add unique index, performance indexes, and required Insert types Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/supabase.ts | 12 +++++----- src/app/api/ratings/route.ts | 2 +- src/app/api/views/route.ts | 6 ++++- .../migrations/00009_contract_address.sql | 12 +++++----- .../00010_contract_address_indexes.sql | 22 +++++++++++++++++++ 5 files changed, 40 insertions(+), 14 deletions(-) create mode 100644 supabase/migrations/00010_contract_address_indexes.sql diff --git a/lib/supabase.ts b/lib/supabase.ts index de046a18..9662f48b 100644 --- a/lib/supabase.ts +++ b/lib/supabase.ts @@ -80,7 +80,7 @@ export interface Database { block_timestamp?: string | null; indexed_at?: string; view_count?: number; - contract_address?: string; + contract_address: string; }; Update: { id?: never; @@ -119,7 +119,7 @@ export interface Database { viewer_address?: string | null; session_id: string; viewed_at?: string; - contract_address?: string; + contract_address: string; }; Update: { id?: never; @@ -162,7 +162,7 @@ export interface Database { log_index: number; block_timestamp?: string | null; indexed_at?: string; - contract_address?: string; + contract_address: string; }; Update: { id?: never; @@ -200,7 +200,7 @@ export interface Database { content: string; created_at?: string; hidden?: boolean; - contract_address?: string; + contract_address: string; }; Update: { id?: never; @@ -234,7 +234,7 @@ export interface Database { log_index: number; block_timestamp?: string | null; indexed_at?: string; - contract_address?: string; + contract_address: string; }; Update: { id?: never; @@ -267,7 +267,7 @@ export interface Database { comment?: string | null; created_at?: string; updated_at?: string; - contract_address?: string; + contract_address: string; }; Update: { id?: never; diff --git a/src/app/api/ratings/route.ts b/src/app/api/ratings/route.ts index 1d2fc02d..5f55a73c 100644 --- a/src/app/api/ratings/route.ts +++ b/src/app/api/ratings/route.ts @@ -187,7 +187,7 @@ export async function POST(req: NextRequest) { updated_at: new Date().toISOString(), contract_address: STORY_FACTORY.toLowerCase(), }, - { onConflict: "storyline_id,rater_address" }, + { onConflict: "storyline_id,rater_address,contract_address" }, ); if (upsertError) { diff --git a/src/app/api/views/route.ts b/src/app/api/views/route.ts index e57923de..7413f0f5 100644 --- a/src/app/api/views/route.ts +++ b/src/app/api/views/route.ts @@ -120,6 +120,7 @@ export async function POST(req: NextRequest) { let dedupQuery = (serverClient.from("page_views") as any) .select("id") .eq("storyline_id", storylineId) + .eq("contract_address", STORY_FACTORY.toLowerCase()) .eq("session_id", sessionId) .gte("viewed_at", oneHourAgo) .limit(1); @@ -151,7 +152,10 @@ export async function POST(req: NextRequest) { // Increment denormalized counter (storyline-level views only) if (plotVal === null) { // eslint-disable-next-line @typescript-eslint/no-explicit-any - await (serverClient.rpc as any)("increment_view_count", { sid: storylineId }).catch(() => { + await (serverClient.rpc as any)("increment_view_count", { + sid: storylineId, + caddr: STORY_FACTORY.toLowerCase(), + }).catch(() => { // Ignore — counter will be slightly behind but page_views table is authoritative }); } diff --git a/supabase/migrations/00009_contract_address.sql b/supabase/migrations/00009_contract_address.sql index c6585571..d82a31d5 100644 --- a/supabase/migrations/00009_contract_address.sql +++ b/supabase/migrations/00009_contract_address.sql @@ -1,12 +1,12 @@ -- Add contract_address to all tables for multi-contract support -- Default existing rows to old contract address (lowercase) -ALTER TABLE storylines ADD COLUMN contract_address TEXT NOT NULL DEFAULT '0x05c4d59529807316d6fa09cdaa509addfe85b474'; -ALTER TABLE plots ADD COLUMN contract_address TEXT NOT NULL DEFAULT '0x05c4d59529807316d6fa09cdaa509addfe85b474'; -ALTER TABLE donations ADD COLUMN contract_address TEXT NOT NULL DEFAULT '0x05c4d59529807316d6fa09cdaa509addfe85b474'; -ALTER TABLE ratings ADD COLUMN contract_address TEXT NOT NULL DEFAULT '0x05c4d59529807316d6fa09cdaa509addfe85b474'; -ALTER TABLE comments ADD COLUMN contract_address TEXT NOT NULL DEFAULT '0x05c4d59529807316d6fa09cdaa509addfe85b474'; -ALTER TABLE page_views ADD COLUMN contract_address TEXT NOT NULL DEFAULT '0x05c4d59529807316d6fa09cdaa509addfe85b474'; +ALTER TABLE storylines ADD COLUMN contract_address TEXT NOT NULL DEFAULT '0x6b8d38af1773dd162ebc6f4a8eb923f3c669605d'; +ALTER TABLE plots ADD COLUMN contract_address TEXT NOT NULL DEFAULT '0x6b8d38af1773dd162ebc6f4a8eb923f3c669605d'; +ALTER TABLE donations ADD COLUMN contract_address TEXT NOT NULL DEFAULT '0x6b8d38af1773dd162ebc6f4a8eb923f3c669605d'; +ALTER TABLE ratings ADD COLUMN contract_address TEXT NOT NULL DEFAULT '0x6b8d38af1773dd162ebc6f4a8eb923f3c669605d'; +ALTER TABLE comments ADD COLUMN contract_address TEXT NOT NULL DEFAULT '0x6b8d38af1773dd162ebc6f4a8eb923f3c669605d'; +ALTER TABLE page_views ADD COLUMN contract_address TEXT NOT NULL DEFAULT '0x6b8d38af1773dd162ebc6f4a8eb923f3c669605d'; -- After backfilling existing rows, change default to empty string -- Indexers will always explicitly pass the value diff --git a/supabase/migrations/00010_contract_address_indexes.sql b/supabase/migrations/00010_contract_address_indexes.sql new file mode 100644 index 00000000..1e9e3f7a --- /dev/null +++ b/supabase/migrations/00010_contract_address_indexes.sql @@ -0,0 +1,22 @@ +-- Fix ratings unique constraint to include contract_address for multi-contract upsert +ALTER TABLE ratings DROP CONSTRAINT IF EXISTS ratings_storyline_id_rater_address_key; +ALTER TABLE ratings ADD CONSTRAINT ratings_storyline_id_rater_address_contract_address_key + UNIQUE (storyline_id, rater_address, contract_address); + +-- Update increment_view_count RPC to scope by contract_address +CREATE OR REPLACE FUNCTION increment_view_count(sid INTEGER, caddr TEXT) +RETURNS void AS $$ +BEGIN + UPDATE storylines + SET view_count = view_count + 1 + WHERE storyline_id = sid AND contract_address = caddr; +END; +$$ LANGUAGE plpgsql; + +-- Performance indexes on contract_address for filtered queries +CREATE INDEX idx_storylines_contract ON storylines (contract_address); +CREATE INDEX idx_plots_contract ON plots (contract_address); +CREATE INDEX idx_donations_contract ON donations (contract_address); +CREATE INDEX idx_ratings_contract ON ratings (contract_address); +CREATE INDEX idx_comments_contract ON comments (contract_address); +CREATE INDEX idx_page_views_contract ON page_views (contract_address);