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
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "plotlink",
"version": "1.41.1",
"version": "1.41.2",
"private": true,
"workspaces": [
"packages/*"
Expand Down
74 changes: 74 additions & 0 deletions src/app/api/airdrop/leaderboard/route.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// @vitest-environment node
import { describe, expect, it, vi } from "vitest";
import { NextRequest } from "next/server";

const mockRpc = vi.fn();
const mockUsers = vi.fn();

vi.mock("../../../../../lib/supabase", () => ({
createServerClient: () => ({
rpc: mockRpc,
from: (table: string) => {
if (table === "pl_referral_codes") return { select: () => ({ in: mockUsers }) };
return {};
},
}),
}));
vi.mock("../../../../../lib/airdrop/config", () => ({
getAirdropConfig: () => ({
CAMPAIGN_START: new Date("2026-07-01"),
CAMPAIGN_END: new Date("2026-10-01"),
MIN_REFERRAL_THRESHOLD: 50,
REFERRAL_MULTIPLIER_PER_REF: 0.2,
REFERRAL_MULTIPLIER_CAP: 3.0,
}),
}));

import { GET } from "./route";

describe("GET /api/airdrop/leaderboard", () => {
it("returns entries ordered by weighted_spend DESC", async () => {
mockRpc.mockResolvedValue({
data: [
{ address: "alice", weighted_spend: 200, buy_volume: 100, qualified_refs: 1, has_fc_bonus: 1, multiplier: 2, community_total: 500 },
{ address: "bob", weighted_spend: 300, buy_volume: 150, qualified_refs: 2, has_fc_bonus: 0, multiplier: 1.4, community_total: 500 },
],
error: null,
});
mockUsers.mockResolvedValue({ data: [] });

const req = new NextRequest("http://localhost/api/airdrop/leaderboard");
const res = await GET(req);
expect(res.status).toBe(200);
const data = await res.json();
expect(data.entries[0].address).toBe("bob");
expect(data.entries[1].address).toBe("alice");
expect(data.entries[0].totalPoints).toBe(300);
});

it("returns empty when no eligible wallets", async () => {
mockRpc.mockResolvedValue({ data: [], error: null });
mockUsers.mockResolvedValue({ data: [] });

const req = new NextRequest("http://localhost/api/airdrop/leaderboard");
const res = await GET(req);
const data = await res.json();
expect(data.entries).toHaveLength(0);
expect(data.totalParticipants).toBe(0);
});

it("passes correct config params to weighted_spend RPC", async () => {
mockRpc.mockResolvedValue({ data: [], error: null });
mockUsers.mockResolvedValue({ data: [] });

const req = new NextRequest("http://localhost/api/airdrop/leaderboard");
await GET(req);
expect(mockRpc).toHaveBeenCalledWith("weighted_spend", expect.objectContaining({
p_campaign_start: expect.any(String),
p_campaign_end: expect.any(String),
p_min_referral_threshold: 50,
p_multiplier_per_ref: 0.2,
p_multiplier_cap: 3.0,
}));
});
});
98 changes: 98 additions & 0 deletions src/app/api/airdrop/points/route.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// @vitest-environment node
import { describe, expect, it, vi } from "vitest";
import { NextRequest } from "next/server";

const mockPoints = vi.fn();
const mockAllPoints = vi.fn();
const mockStreak = vi.fn();
const mockRefCode = vi.fn();
const mockReferredBy = vi.fn();
const mockRefCount = vi.fn();

vi.mock("../../../../../lib/supabase", () => ({
createServerClient: () => ({
from: (table: string) => {
if (table === "pl_points") {
return {
select: (cols: string) => {
if (cols.includes("action")) return { eq: () => mockPoints() };
return mockAllPoints();
},
};
}
if (table === "pl_streaks") return { select: () => ({ eq: () => ({ single: mockStreak }) }) };
if (table === "pl_referral_codes") return { select: () => ({ eq: () => ({ single: mockRefCode }) }) };
if (table === "pl_referrals") {
return {
select: (cols: string, opts?: { count?: string; head?: boolean }) => {
if (opts?.count) return { eq: mockRefCount };
return { eq: () => ({ single: mockReferredBy }) };
},
};
}
return {};
},
}),
}));
vi.mock("../../../../../lib/airdrop/config", () => ({
getAirdropConfig: () => ({
CAMPAIGN_START: new Date("2026-07-01"),
CAMPAIGN_END: new Date("2026-10-01"),
POOL_AMOUNT: 200_000,
MILESTONES: {
BRONZE: { mcap: 100_000, pct: 10 },
SILVER: { mcap: 1_000_000, pct: 30 },
GOLD: { mcap: 5_000_000, pct: 50 },
DIAMOND: { mcap: 10_000_000, pct: 100 },
},
}),
}));

import { GET } from "./route";

describe("GET /api/airdrop/points", () => {
it("returns deprecated shape with buy_volume_plot + deprecation headers", async () => {
mockPoints.mockResolvedValue({ data: [{ action: "buy", points: 100 }] });
mockAllPoints.mockResolvedValue({ data: [{ points: 100 }, { points: 200 }] });
mockStreak.mockResolvedValue({ data: null });
mockRefCode.mockResolvedValue({ data: { code: "mycode", is_farcaster_username: false } });
mockReferredBy.mockResolvedValue({ data: null });
mockRefCount.mockResolvedValue({ count: 3 });

const req = new NextRequest("http://localhost/api/airdrop/points?address=0xabc");
const res = await GET(req);
expect(res.status).toBe(200);

const data = await res.json();
expect(data.address).toBe("0xabc");
expect(data.buy_volume_plot).toBeDefined();
expect(data.fetched_at).toBeDefined();
expect(data.totalPoints).toBeDefined();
expect(data.referral.code).toBe("mycode");

expect(res.headers.get("Deprecation")).toBe("true");
expect(res.headers.get("Link")).toContain("/api/airdrop/projection");
});

it("handles non-existent address gracefully", async () => {
mockPoints.mockResolvedValue({ data: [] });
mockAllPoints.mockResolvedValue({ data: [] });
mockStreak.mockResolvedValue({ data: null });
mockRefCode.mockResolvedValue({ data: null });
mockReferredBy.mockResolvedValue({ data: null });
mockRefCount.mockResolvedValue({ count: 0 });

const req = new NextRequest("http://localhost/api/airdrop/points?address=0xnone");
const res = await GET(req);
expect(res.status).toBe(200);
const data = await res.json();
expect(data.totalPoints).toBe(0);
expect(data.buy_volume_plot).toBe(0);
});

it("returns 400 when address missing", async () => {
const req = new NextRequest("http://localhost/api/airdrop/points");
const res = await GET(req);
expect(res.status).toBe(400);
});
});
76 changes: 76 additions & 0 deletions src/app/api/airdrop/status/route.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// @vitest-environment node
import { describe, expect, it, vi } from "vitest";

const mockPriceSingle = vi.fn();
vi.mock("../../../../../lib/supabase", () => ({
createServerClient: () => ({
from: (table: string) => {
if (table === "pl_daily_prices") {
return { select: () => ({ order: () => ({ limit: () => ({ single: mockPriceSingle }) }) }) };
}
if (table === "pl_activations") {
const allCount = 15;
const eligibleCount = 12;
return {
select: () => ({
not: () => {
const base = Promise.resolve({ count: allCount });
return Object.assign(base, {
eq: () => Promise.resolve({ count: eligibleCount }),
});
},
}),
};
}
return {};
},
}),
}));
vi.mock("../../../../../lib/airdrop/config", () => ({
getAirdropConfig: () => ({
CAMPAIGN_START: new Date("2026-07-01"),
CAMPAIGN_END: new Date("2026-10-01"),
POOL_AMOUNT: 200_000,
MILESTONES: {
BRONZE: { mcap: 100_000, pct: 10 },
SILVER: { mcap: 1_000_000, pct: 30 },
GOLD: { mcap: 5_000_000, pct: 50 },
DIAMOND: { mcap: 10_000_000, pct: 100 },
},
LOCKER_TX: null,
SIWE_DOMAIN: "plotlink.xyz",
SIWE_URI: "https://plotlink.xyz/airdrop",
SIWE_STATEMENT: "PlotLink Buy-Back Sprint activation",
SIWE_CHAIN_ID: 8453,
PLOTLINK_X_HANDLE: "plotlinkxyz",
PLOTLINK_FC_FID: 0,
}),
}));
vi.mock("../../../../../lib/usd-price", () => ({ getPlotUsdPrice: () => Promise.resolve(0.037) }));

import { GET } from "./route";


describe("GET /api/airdrop/status", () => {
it("returns v5 shape with milestones + activation counts + env_check", async () => {
mockPriceSingle.mockResolvedValue({ data: { price_usd: 0.037, mcap_usd: 37000 } });

const res = await GET();
expect(res.status).toBe(200);
const data = await res.json();
expect(data.milestones.bronze.mcap).toBe(100_000);
expect(data.milestones.diamond.mcap).toBe(10_000_000);
expect(data.poolAmount).toBe(200_000);
expect(data.activation_count).toBe(15);
expect(data.eligible_activation_count).toBe(12);
expect(typeof data.env_check.all_present).toBe("boolean");
});

it("env_check.all_present is false when required env vars missing", async () => {
mockPriceSingle.mockResolvedValue({ data: null });

const res = await GET();
const data = await res.json();
expect(data.env_check.all_present).toBe(false);
});
});
Loading