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
115 changes: 115 additions & 0 deletions lib/airdrop/award.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/**
* Airdrop point award helpers (#884)
*
* Convenience functions for awarding write and rate points,
* called from indexer/backfill/rating endpoints.
*/

import { AIRDROP_CONFIG } from "./config";
import { getStreakBoost } from "./streak";
import { createServiceRoleClient } from "../supabase";

/**
* Award write points (50 PL) for publishing a storyline.
* Idempotent via metadata.storyline_id dedup.
*/
export async function awardWritePoints(
writerAddress: string,
storylineId: number,
timestamp?: Date,
): Promise<void> {
const now = timestamp ?? new Date();
if (now < AIRDROP_CONFIG.CAMPAIGN_START || now > AIRDROP_CONFIG.CAMPAIGN_END) return;

const supabase = createServiceRoleClient();
if (!supabase) return;

const address = writerAddress.toLowerCase();

// Dedup check
const { data: existing } = await supabase
.from("pl_points")
.select("id")
.eq("action", "write")
.eq("address", address)
.eq("metadata->>storyline_id", String(storylineId))
.limit(1);

if (existing && existing.length > 0) return;

// Look up streak
const { data: streak } = await supabase
.from("pl_streaks")
.select("current_streak")
.eq("address", address)
.single();

const boost = getStreakBoost(streak?.current_streak ?? 0);
const points = AIRDROP_CONFIG.POINTS.WRITE_FLAT * (1 + boost);

await supabase.from("pl_points").insert({
address,
action: "write",
points,
metadata: { storyline_id: storylineId },
});
}

/**
* Award rate points (5 PL) for rating a story.
* Capped at RATE_DAILY_CAP per day per address.
* Dedup via metadata.storyline_id + address.
*/
export async function awardRatePoints(
raterAddress: string,
storylineId: number,
): Promise<void> {
const now = new Date();
if (now < AIRDROP_CONFIG.CAMPAIGN_START || now > AIRDROP_CONFIG.CAMPAIGN_END) return;

const supabase = createServiceRoleClient();
if (!supabase) return;

const address = raterAddress.toLowerCase();

// Dedup check (one rating per story per user)
const { data: existing } = await supabase
.from("pl_points")
.select("id")
.eq("action", "rate")
.eq("address", address)
.eq("metadata->>storyline_id", String(storylineId))
.limit(1);

if (existing && existing.length > 0) return;

// Daily cap check
const todayStart = new Date();
todayStart.setUTCHours(0, 0, 0, 0);

const { count } = await supabase
.from("pl_points")
.select("id", { count: "exact", head: true })
.eq("action", "rate")
.eq("address", address)
.gte("created_at", todayStart.toISOString());

if ((count ?? 0) >= AIRDROP_CONFIG.POINTS.RATE_DAILY_CAP) return;

// Look up streak
const { data: streak } = await supabase
.from("pl_streaks")
.select("current_streak")
.eq("address", address)
.single();

const boost = getStreakBoost(streak?.current_streak ?? 0);
const points = AIRDROP_CONFIG.POINTS.RATE_FLAT * (1 + boost);

await supabase.from("pl_points").insert({
address,
action: "rate",
points,
metadata: { storyline_id: storylineId },
});
}
4 changes: 4 additions & 0 deletions src/app/api/cron/backfill/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { hashContent } from "../../../../../lib/content";
import { detectWriterType } from "../../../../../lib/contracts/erc8004";
import { reconcileStorylinePlotCount } from "../../../../../lib/reconcile";
import { notifyNewPlot, notifyNewStoryline } from "../../../../../lib/notifications.server";
import { awardWritePoints } from "../../../../../lib/airdrop/award";
import type { Database } from "../../../../../lib/supabase";

const IPFS_GATEWAY = "https://ipfs.filebase.io/ipfs/";
Expand Down Expand Up @@ -167,6 +168,9 @@ export async function GET(req: Request) {
// Notify users about the new storyline
const args = decoded.args as { storylineId: bigint; title: string; writer: `0x${string}` };
notifyNewStoryline(Number(args.storylineId), args.title, args.writer).catch(() => {});
// Award airdrop write points (non-blocking)
const scBlockTs = await getCachedBlockTimestamp(log.blockNumber!);
awardWritePoints(args.writer, Number(args.storylineId), scBlockTs ? new Date(Number(scBlockTs) * 1000) : undefined).catch(() => {});
}
} else if (decoded.eventName === "PlotChained") {
const failed = await processPlotChained(
Expand Down
4 changes: 4 additions & 0 deletions src/app/api/index/storyline/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { NextResponse } from "next/server";
import { type Hex, decodeEventLog, encodeEventTopics } from "viem";
import { publicClient, getReceiptWithRetry } from "../../../../../lib/rpc";

Check warning on line 3 in src/app/api/index/storyline/route.ts

View workflow job for this annotation

GitHub Actions / lint-and-typecheck

'getReceiptWithRetry' is defined but never used
import { createServerClient } from "../../../../../lib/supabase";
import { validateRecentTx } from "../../../../../lib/index-auth";
import {
Expand All @@ -13,6 +13,7 @@
import { GENRES, LANGUAGES } from "../../../../../lib/genres";
import type { Database } from "../../../../../lib/supabase";
import { reconcileStorylinePlotCount } from "../../../../../lib/reconcile";
import { awardWritePoints } from "../../../../../lib/airdrop/award";

const IPFS_GATEWAY = "https://ipfs.filebase.io/ipfs/";
const IPFS_TIMEOUT_MS = 10_000;
Expand Down Expand Up @@ -183,5 +184,8 @@
// Reconcile plot_count from actual plots rows (prevents genesis double-count)
await reconcileStorylinePlotCount(supabase, Number(storylineId));

// Award airdrop write points (non-blocking, using on-chain timestamp)
awardWritePoints(writer, Number(storylineId), new Date(Number(blockTimestamp) * 1000)).catch(() => {});

return NextResponse.json({ success: true });
}
4 changes: 4 additions & 0 deletions src/app/api/ratings/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { publicClient } from "../../../../lib/rpc";
import { createServerClient, supabase } from "../../../../lib/supabase";
import { erc20Abi } from "../../../../lib/price";
import { STORY_FACTORY } from "../../../../lib/contracts/constants";
import { awardRatePoints } from "../../../../lib/airdrop/award";

const MAX_COMMENT_LENGTH = 500;

Expand Down Expand Up @@ -189,5 +190,8 @@ export async function POST(req: NextRequest) {
return error(`Database error: ${upsertError.message}`, 500);
}

// Award airdrop rate points (non-blocking)
awardRatePoints(raterAddress, storylineId).catch(() => {});

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