diff --git a/lib/airdrop/award.ts b/lib/airdrop/award.ts index 22f2d671..9e09008d 100644 --- a/lib/airdrop/award.ts +++ b/lib/airdrop/award.ts @@ -1,115 +1,14 @@ -/** - * 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, + _writerAddress: string, + _storylineId: number, + _timestamp?: Date, ): Promise { - 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 }, - }); + return; } -/** - * 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, + _raterAddress: string, + _storylineId: number, ): Promise { - 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 }, - }); + return; } diff --git a/lib/airdrop/points.ts b/lib/airdrop/points.ts index 881bfc06..0e799c75 100644 --- a/lib/airdrop/points.ts +++ b/lib/airdrop/points.ts @@ -1,36 +1,10 @@ -/** - * Airdrop points helpers (#881) - * - * Computes buy points with streak boost, and referral points. - */ - -import { AIRDROP_CONFIG } from "./config"; import { getStreakBoost } from "./streak"; export { getStreakBoost }; -/** - * Compute buy points for a trade. - * Points = PLOT spent × BUY_PER_PLOT × (1 + streak boost) - */ export function computeBuyPoints( plotSpent: number, - currentStreak: number, -): number { - const base = plotSpent * AIRDROP_CONFIG.POINTS.BUY_PER_PLOT; - const boost = getStreakBoost(currentStreak); - return base * (1 + boost); -} - -/** - * Compute referral points (percentage of buyer's boosted buy points). - * Also boosted by the referrer's own streak. - */ -export function computeReferralPoints( - buyerBoostedPoints: number, - referrerStreak: number, + _currentStreak: number, ): number { - const base = buyerBoostedPoints * (AIRDROP_CONFIG.POINTS.REFERRAL_PCT / 100); - const boost = getStreakBoost(referrerStreak); - return base * (1 + boost); + return plotSpent; } diff --git a/package.json b/package.json index 0173728e..ef61efdd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "plotlink", - "version": "1.30.0", + "version": "1.30.1", "private": true, "workspaces": [ "packages/*" diff --git a/src/app/api/airdrop/checkin/route.ts b/src/app/api/airdrop/checkin/route.ts index b92f0906..160d08db 100644 --- a/src/app/api/airdrop/checkin/route.ts +++ b/src/app/api/airdrop/checkin/route.ts @@ -1,141 +1,3 @@ -/** - * Streak check-in endpoint (#882) - * - * POST /api/airdrop/checkin - * Body: { message: string, signature: string } - * - * Verifies SIWE signature, updates streak in pl_streaks. - */ - -import { NextResponse } from "next/server"; -import { createServerClient } from "../../../../../lib/supabase"; -import { AIRDROP_CONFIG } from "../../../../../lib/airdrop/config"; -import { getStreakBoost, dropOneTier, getNextTier } from "../../../../../lib/airdrop/streak"; -import { verifyWalletOwnership } from "../../../../../lib/airdrop/verify-wallet"; -import { checkRateLimit, getClientIp } from "../../../../../lib/rate-limit"; - -export async function POST(req: Request) { - const ip = getClientIp(req); - if (!(await checkRateLimit(ip, "airdrop/checkin"))) { - return NextResponse.json({ error: "Too many requests" }, { status: 429 }); - } - - const supabase = createServerClient(); - if (!supabase) { - return NextResponse.json({ error: "Supabase not configured" }, { status: 500 }); - } - - let message: string; - let signature: `0x${string}`; - try { - const body = await req.json(); - message = body.message; - signature = body.signature; - if (!message || !signature) throw new Error("missing fields"); - } catch { - return NextResponse.json({ error: "Invalid request body" }, { status: 400 }); - } - - // Verify wallet ownership via SIWE signature - const claimedAddress = await verifyWalletOwnership(message, signature); - if (!claimedAddress) { - return NextResponse.json({ error: "Invalid signature" }, { status: 401 }); - } - - const now = new Date(); - - // Campaign window check - if (now < AIRDROP_CONFIG.CAMPAIGN_START || now > AIRDROP_CONFIG.CAMPAIGN_END) { - return NextResponse.json({ error: "Campaign not active" }, { status: 400 }); - } - - // Fetch or create streak record - const { data: existing } = await supabase - .from("pl_streaks") - .select("*") - .eq("address", claimedAddress) - .single(); - - const todayUtc = now.toISOString().slice(0, 10); // YYYY-MM-DD - - if (existing?.last_checkin) { - const lastCheckin = new Date(existing.last_checkin); - const lastCheckinDay = lastCheckin.toISOString().slice(0, 10); - - // Reject if same calendar day (UTC) - if (lastCheckinDay === todayUtc) { - return NextResponse.json({ - error: "Already checked in today", - streak: existing.current_streak, - boostPercent: getStreakBoost(existing.current_streak) * 100, - nextTier: getNextTier(existing.current_streak), - checkedInToday: true, - }, { status: 429 }); - } - - // Reject if less than 30 minutes ago - const minutesSince = (now.getTime() - lastCheckin.getTime()) / (1000 * 60); - if (minutesSince < AIRDROP_CONFIG.STREAK_MIN_GAP_MINUTES) { - return NextResponse.json({ - error: `Must wait ${AIRDROP_CONFIG.STREAK_MIN_GAP_MINUTES} minutes between check-ins`, - }, { status: 429 }); - } - } - - let newStreak: number; - - if (!existing) { - // First ever check-in - newStreak = 1; - const { error } = await supabase.from("pl_streaks").insert({ - address: claimedAddress, - current_streak: newStreak, - last_checkin: now.toISOString(), - longest_streak: newStreak, - }); - if (error) { - console.error("[checkin] Insert failed:", error.message); - return NextResponse.json({ error: "Check-in failed" }, { status: 500 }); - } - } else { - const lastCheckin = existing.last_checkin ? new Date(existing.last_checkin) : null; - - if (lastCheckin) { - const lastDay = lastCheckin.toISOString().slice(0, 10); - const yesterdayUtc = new Date(now.getTime() - 86400000).toISOString().slice(0, 10); - - if (lastDay === yesterdayUtc) { - // Consecutive day — increment streak - newStreak = existing.current_streak + 1; - } else { - // Missed 2+ days — drop to previous tier threshold - newStreak = dropOneTier(existing.current_streak); - } - } else { - newStreak = 1; - } - - const longestStreak = Math.max(existing.longest_streak, newStreak); - - const { error } = await supabase - .from("pl_streaks") - .update({ - current_streak: newStreak, - last_checkin: now.toISOString(), - longest_streak: longestStreak, - }) - .eq("address", claimedAddress); - - if (error) { - console.error("[checkin] Update failed:", error.message); - return NextResponse.json({ error: "Check-in failed" }, { status: 500 }); - } - } - - return NextResponse.json({ - streak: newStreak, - boostPercent: getStreakBoost(newStreak) * 100, - nextTier: getNextTier(newStreak), - checkedInToday: true, - }); +export async function POST() { + return new Response(null, { status: 410 }); } diff --git a/src/app/api/cron/airdrop-points/route.ts b/src/app/api/cron/airdrop-points/route.ts index c4f221be..88978478 100644 --- a/src/app/api/cron/airdrop-points/route.ts +++ b/src/app/api/cron/airdrop-points/route.ts @@ -1,17 +1,9 @@ -/** - * Airdrop buy-points sync cron (#881) - * - * Syncs trade_history mint events → pl_points for buy + referral points. - * Schedule: every 5 min - */ - import { NextResponse } from "next/server"; import { createServerClient } from "../../../../../lib/supabase"; import { ZAP_PLOTLINK } from "../../../../../lib/contracts/constants"; import { AIRDROP_CONFIG } from "../../../../../lib/airdrop/config"; -import { computeBuyPoints, computeReferralPoints } from "../../../../../lib/airdrop/points"; +import { computeBuyPoints } from "../../../../../lib/airdrop/points"; -/** Fail closed in production when CRON_SECRET is unset */ function verifyCron(req: Request): boolean { const secret = process.env.CRON_SECRET; if (!secret) { @@ -42,7 +34,6 @@ async function handler(req: Request) { const zapAddress = ZAP_PLOTLINK.toLowerCase(); - // Fetch mint trades within the campaign window const { data: trades, error: tradesErr } = await supabase .from("trade_history") .select("id, user_address, reserve_amount, block_timestamp") @@ -60,12 +51,10 @@ async function handler(req: Request) { return NextResponse.json({ message: "No trades to process", processed: 0 }); } - // Filter out ZAP_PLOTLINK self-mints const eligible = trades.filter( (t) => t.user_address && t.user_address.toLowerCase() !== zapAddress, ); - // Fetch existing trade_ids from pl_points to dedup const tradeIds = eligible.map((t) => t.id); const { data: existing } = await supabase .from("pl_points") @@ -82,46 +71,7 @@ async function handler(req: Request) { .filter(Boolean), ); - // Collect unique buyer addresses for streak lookup - const buyerAddresses = [ - ...new Set(eligible.filter((t) => !processedTradeIds.has(String(t.id))).map((t) => t.user_address!.toLowerCase())), - ]; - - // Batch-fetch streaks - const { data: streaks } = await supabase - .from("pl_streaks") - .select("address, current_streak") - .in("address", buyerAddresses); - - const streakMap = new Map( - (streaks ?? []).map((s) => [s.address.toLowerCase(), s.current_streak]), - ); - - // Batch-fetch referrals for buyers - const { data: referrals } = await supabase - .from("pl_referrals") - .select("referred_address, referrer_address") - .in("referred_address", buyerAddresses); - - const referralMap = new Map( - (referrals ?? []).map((r) => [r.referred_address.toLowerCase(), r.referrer_address.toLowerCase()]), - ); - - // Collect referrer addresses for streak lookup - const referrerAddresses = [...new Set(referralMap.values())]; - const { data: referrerStreaks } = referrerAddresses.length > 0 - ? await supabase - .from("pl_streaks") - .select("address, current_streak") - .in("address", referrerAddresses) - : { data: [] }; - - const referrerStreakMap = new Map( - (referrerStreaks ?? []).map((s) => [s.address.toLowerCase(), s.current_streak]), - ); - let buyCount = 0; - let referralCount = 0; const inserts: Array<{ address: string; action: string; @@ -133,11 +83,7 @@ async function handler(req: Request) { if (processedTradeIds.has(String(trade.id))) continue; const address = trade.user_address!.toLowerCase(); - const plotSpent = trade.reserve_amount; - const buyerStreak = streakMap.get(address) ?? 0; - - // Buy points - const buyPoints = computeBuyPoints(plotSpent, buyerStreak); + const buyPoints = computeBuyPoints(trade.reserve_amount, 0); inserts.push({ address, action: "buy", @@ -145,20 +91,6 @@ async function handler(req: Request) { metadata: { trade_id: trade.id }, }); buyCount++; - - // Referral points - const referrer = referralMap.get(address); - if (referrer) { - const referrerStreak = referrerStreakMap.get(referrer) ?? 0; - const refPoints = computeReferralPoints(buyPoints, referrerStreak); - inserts.push({ - address: referrer, - action: "referral", - points: refPoints, - metadata: { trade_id: trade.id, referred_address: address }, - }); - referralCount++; - } } if (inserts.length > 0) { @@ -169,10 +101,10 @@ async function handler(req: Request) { } } - console.info(`[airdrop-points] Processed ${buyCount} buys, ${referralCount} referrals`); + console.info(`[airdrop-points] Processed ${buyCount} buys`); return NextResponse.json({ message: "Points synced", - processed: { buys: buyCount, referrals: referralCount }, + processed: { buys: buyCount }, }); }