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
42 changes: 18 additions & 24 deletions lib/ranking.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { type Address, formatUnits } from "viem";
import { get24hPriceChange, getTokenTVL } from "./price";
import { STORY_FACTORY } from "./contracts/constants";
import type { Storyline } from "./supabase";
import type { Database, Storyline } from "./supabase";
import type { SupabaseClient } from "@supabase/supabase-js";

interface RankedStoryline extends Storyline {
Expand Down Expand Up @@ -57,9 +57,8 @@ function computeTrendScore(
}

/** Shared: fetch storyline candidates + batch ratings */
async function fetchCandidatesAndRatings(supabase: SupabaseClient, writerType?: number) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let q = (supabase.from("storylines") as any)
async function fetchCandidatesAndRatings(supabase: SupabaseClient<Database>, writerType?: number) {
let q = supabase.from("storylines")
.select("*")
.eq("hidden", false)
.eq("sunset", false)
Expand All @@ -75,16 +74,15 @@ async function fetchCandidatesAndRatings(supabase: SupabaseClient, writerType?:

// Batch: fetch all ratings for candidate storyline IDs in one query
const storylineIds = storylines.map((sl) => sl.storyline_id);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { data: allRatings } = await (supabase.from("ratings") as any)
const { data: allRatings } = await supabase.from("ratings")
.select("storyline_id, rating")
.in("storyline_id", storylineIds)
.eq("contract_address", STORY_FACTORY.toLowerCase());

const ratingMap = new Map<number, number>();
if (allRatings) {
const grouped = new Map<number, number[]>();
for (const r of allRatings as { storyline_id: number; rating: number }[]) {
for (const r of allRatings) {
const arr = grouped.get(r.storyline_id) ?? [];
arr.push(r.rating);
grouped.set(r.storyline_id, arr);
Expand Down Expand Up @@ -118,7 +116,7 @@ async function enrichWithOnChain(
* Fetch trending storylines ranked by composite score.
*/
export async function getTrendingStorylines(
supabase: SupabaseClient,
supabase: SupabaseClient<Database>,
limit = 20,
writerType?: number,
offset = 0,
Expand Down Expand Up @@ -158,7 +156,7 @@ export async function getTrendingStorylines(
* baseline denominator — acceleration comes from rating + plot signals.
*/
export async function getRisingStorylines(
supabase: SupabaseClient,
supabase: SupabaseClient<Database>,
limit = 20,
writerType?: number,
offset = 0,
Expand All @@ -181,31 +179,27 @@ export async function getRisingStorylines(
const storylineIds = eligible.map((sl) => sl.storyline_id);

// Batch: windowed ratings
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { data: recentRatings } = await (supabase.from("ratings") as any)
const { data: recentRatings } = await supabase.from("ratings")
.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)
const { data: priorRatings } = await supabase.from("ratings")
.select("storyline_id, rating")
.in("storyline_id", storylineIds)
.eq("contract_address", STORY_FACTORY.toLowerCase())
.gte("updated_at", sixDaysAgo)
.lt("updated_at", threeDaysAgo);

// Batch: windowed plot counts
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { data: recentPlots } = await (supabase.from("plots") as any)
const { data: recentPlots } = await supabase.from("plots")
.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)
const { data: priorPlots } = await supabase.from("plots")
.select("storyline_id")
.in("storyline_id", storylineIds)
.eq("contract_address", STORY_FACTORY.toLowerCase())
Expand All @@ -214,14 +208,14 @@ export async function getRisingStorylines(

function avgFromRows(rows: { storyline_id: number; rating: number }[] | null, slId: number): number {
if (!rows) return 0;
const filtered = rows.filter((r) => r.storyline_id === slId);
const filtered = rows.filter((r: { storyline_id: number }) => r.storyline_id === slId);
if (filtered.length === 0) return 0;
return filtered.reduce((s, r) => s + r.rating, 0) / filtered.length;
return filtered.reduce((s: number, r: { rating: number }) => s + r.rating, 0) / filtered.length;
}

function countFromRows(rows: { storyline_id: number }[] | null, slId: number): number {
if (!rows) return 0;
return rows.filter((r) => r.storyline_id === slId).length;
return rows.filter((r: { storyline_id: number }) => r.storyline_id === slId).length;
}

// Single parallel batch for all on-chain reads
Expand All @@ -233,8 +227,8 @@ export async function getRisingStorylines(
const { priceChange, tvlRaw, tvlDecimals } = onChainResults[i];

// Recent window composite (all 4 signals)
const recentAvgRating = avgFromRows(recentRatings as { storyline_id: number; rating: number }[] | null, sl.storyline_id);
const recentPlotCount = countFromRows(recentPlots as { storyline_id: number }[] | null, sl.storyline_id);
const recentAvgRating = avgFromRows(recentRatings, sl.storyline_id);
const recentPlotCount = countFromRows(recentPlots, sl.storyline_id);
const recentScore = computeTrendScore(
recentAvgRating,
priceChange,
Expand All @@ -245,8 +239,8 @@ export async function getRisingStorylines(
);

// Prior window composite (same 4 signals, same TVL + price as baseline)
const priorAvgRating = avgFromRows(priorRatings as { storyline_id: number; rating: number }[] | null, sl.storyline_id);
const priorPlotCount = countFromRows(priorPlots as { storyline_id: number }[] | null, sl.storyline_id);
const priorAvgRating = avgFromRows(priorRatings, sl.storyline_id);
const priorPlotCount = countFromRows(priorPlots, sl.storyline_id);
const priorScore = computeTrendScore(
priorAvgRating,
priceChange, // same baseline — acceleration from rating/plot signals
Expand Down
39 changes: 39 additions & 0 deletions lib/supabase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export interface Database {
view_count?: number;
contract_address?: string;
};
Relationships: [];
};
page_views: {
Row: {
Expand Down Expand Up @@ -130,6 +131,7 @@ export interface Database {
viewed_at?: string;
contract_address?: string;
};
Relationships: [];
};
plots: {
Row: {
Expand Down Expand Up @@ -180,6 +182,7 @@ export interface Database {
indexed_at?: string;
contract_address?: string;
};
Relationships: [];
};
comments: {
Row: {
Expand Down Expand Up @@ -212,6 +215,7 @@ export interface Database {
hidden?: boolean;
contract_address?: string;
};
Relationships: [];
};
donations: {
Row: {
Expand Down Expand Up @@ -247,6 +251,7 @@ export interface Database {
indexed_at?: string;
contract_address?: string;
};
Relationships: [];
};
ratings: {
Row: {
Expand Down Expand Up @@ -279,7 +284,41 @@ export interface Database {
updated_at?: string;
contract_address?: string;
};
Relationships: [];
};
backfill_cursor: {
Row: {
id: number;
last_block: number;
updated_at: string;
};
Insert: {
id?: number;
last_block: number;
updated_at?: string;
};
Update: {
id?: number;
last_block?: number;
updated_at?: string;
};
Relationships: [];
};
};
Views: {
[_ in never]: never;
};
Functions: {
increment_view_count: {
Args: { sid: number; caddr: string };
Returns: void;
};
};
Enums: {
[_ in never]: never;
};
CompositeTypes: {
[_ in never]: never;
};
};
}
Expand Down
9 changes: 3 additions & 6 deletions src/app/api/admin/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,11 @@ export async function handleModeration(
);
}

const table = type === "storyline" ? "storylines" : "plots";
const idColumn = type === "storyline" ? "storyline_id" : "id";
const hidden = action === "hide";

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { error: dbError } = await (supabase.from(table) as any)
.update({ hidden })
.eq(idColumn, id);
const { error: dbError } = type === "storyline"
? await supabase.from("storylines").update({ hidden }).eq("storyline_id", id)
: await supabase.from("plots").update({ hidden }).eq("id", id);

if (dbError) {
return NextResponse.json(
Expand Down
12 changes: 4 additions & 8 deletions src/app/api/comments/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,7 @@ export async function GET(req: NextRequest) {
const page = Math.max(Number(req.nextUrl.searchParams.get("page") ?? 1), 1);
const offset = (page - 1) * limit;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { data, error: dbError } = await (db.from("comments") as any)
const { data, error: dbError } = await db.from("comments")
.select("*")
.eq("storyline_id", sid)
.eq("plot_index", pidx)
Expand All @@ -46,8 +45,7 @@ export async function GET(req: NextRequest) {
if (dbError) return error(`Database error: ${dbError.message}`, 500);

// Get total count for pagination
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { count } = await (db.from("comments") as any)
const { count } = await db.from("comments")
.select("id", { count: "exact", head: true })
.eq("storyline_id", sid)
.eq("plot_index", pidx)
Expand Down Expand Up @@ -117,8 +115,7 @@ export async function POST(req: NextRequest) {

// Rate limit: max 1 comment per address per plot per minute
const oneMinuteAgo = new Date(Date.now() - 60 * 1000).toISOString();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { data: recent } = await (serverClient.from("comments") as any)
const { data: recent } = await serverClient.from("comments")
.select("id")
.eq("storyline_id", storylineId)
.eq("plot_index", plotIndex)
Expand All @@ -135,8 +132,7 @@ export async function POST(req: NextRequest) {
}

// Insert comment
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { error: insertError } = await (serverClient.from("comments") as any).insert({
const { error: insertError } = await serverClient.from("comments").insert({
storyline_id: storylineId,
plot_index: plotIndex,
commenter_address: commenterAddress.toLowerCase(),
Expand Down
24 changes: 10 additions & 14 deletions src/app/api/cron/backfill/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,7 @@ export async function GET(req: Request) {
const currentBlock = await publicClient.getBlockNumber();

// Read last processed block from persistent cursor
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { data: cursor } = await (supabase.from("backfill_cursor") as any)
const { data: cursor } = await supabase.from("backfill_cursor")
.select("last_block")
.eq("id", 1)
.single();
Expand Down Expand Up @@ -162,8 +161,7 @@ export async function GET(req: Request) {
}

// Persist cursor — advance to highest block actually scanned
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { error: cursorError } = await (supabase.from("backfill_cursor") as any)
const { error: cursorError } = await supabase.from("backfill_cursor")
.update({ last_block: Number(toBlock), updated_at: new Date().toISOString() })
.eq("id", 1);
if (cursorError) {
Expand All @@ -181,17 +179,15 @@ export async function GET(req: Request) {
});
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type DecodedEvent = any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type SupabaseClient = any;
type DecodedEvent = ReturnType<typeof decodeEventLog<typeof storyFactoryAbi>>;
type BackfillSupabaseClient = NonNullable<ReturnType<typeof createServerClient>>;

async function processStorylineCreated(
decoded: DecodedEvent,
log: Log,
txHash: string,
logIndex: number,
supabase: SupabaseClient,
supabase: BackfillSupabaseClient,
getTimestamp: (blockNumber: bigint) => Promise<string>
) {
const {
Expand All @@ -202,7 +198,7 @@ async function processStorylineCreated(
hasDeadline,
openingCID,
openingHash,
} = decoded.args;
} = decoded.args as { storylineId: bigint; writer: `0x${string}`; tokenAddress: `0x${string}`; title: string; hasDeadline: boolean; openingCID: string; openingHash: `0x${string}` };

const timestampISO = await getTimestamp(log.blockNumber!);
const writerType = await detectWriterType(writer);
Expand Down Expand Up @@ -258,11 +254,11 @@ async function processPlotChained(
log: Log,
txHash: string,
logIndex: number,
supabase: SupabaseClient,
supabase: BackfillSupabaseClient,
getTimestamp: (blockNumber: bigint) => Promise<string>
) {
const { storylineId, plotIndex, writer, contentCID, contentHash } =
decoded.args;
decoded.args as { storylineId: bigint; plotIndex: bigint; writer: `0x${string}`; title: string; contentCID: string; contentHash: `0x${string}` };

const content = await fetchIPFSContent(contentCID);
if (content === null) return; // skip if content unavailable
Expand Down Expand Up @@ -296,10 +292,10 @@ async function processDonation(
log: Log,
txHash: string,
logIndex: number,
supabase: SupabaseClient,
supabase: BackfillSupabaseClient,
getTimestamp: (blockNumber: bigint) => Promise<string>
) {
const { storylineId, donor, amount } = decoded.args;
const { storylineId, donor, amount } = decoded.args as { storylineId: bigint; donor: `0x${string}`; amount: bigint };
const timestampISO = await getTimestamp(log.blockNumber!);

const row: DonationInsert = {
Expand Down
3 changes: 1 addition & 2 deletions src/app/api/index/donation/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,7 @@ export async function POST(req: Request) {
contract_address: STORY_FACTORY.toLowerCase(),
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { error: dbError } = await (supabase.from("donations") as any).upsert(
const { error: dbError } = await supabase.from("donations").upsert(
row,
{ onConflict: "tx_hash,log_index" }
);
Expand Down
3 changes: 1 addition & 2 deletions src/app/api/index/plot/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,7 @@ export async function POST(req: Request) {
contract_address: STORY_FACTORY.toLowerCase(),
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { error: dbError } = await (supabase.from("plots") as any).upsert(
const { error: dbError } = await supabase.from("plots").upsert(
row,
{ onConflict: "tx_hash,log_index" }
);
Expand Down
6 changes: 2 additions & 4 deletions src/app/api/index/storyline/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,7 @@ export async function POST(req: Request) {
contract_address: STORY_FACTORY.toLowerCase(),
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { error: dbError } = await (supabase.from("storylines") as any).upsert(
const { error: dbError } = await supabase.from("storylines").upsert(
storylineRow,
{ onConflict: "tx_hash,log_index" }
);
Expand All @@ -165,8 +164,7 @@ export async function POST(req: Request) {
contract_address: STORY_FACTORY.toLowerCase(),
};

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