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
2 changes: 1 addition & 1 deletion contracts/script/DeployMerkleClaim.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
pragma solidity ^0.8.20;

import "forge-std/Script.sol";
import "../MerkleClaim.sol";
import "../src/MerkleClaim.sol";

contract DeployMerkleClaim is Script {
function run() external {
Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion contracts/test/MerkleClaim.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ pragma solidity ^0.8.20;

import "forge-std/Test.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "../MerkleClaim.sol";
import "../src/MerkleClaim.sol";

contract MockPLOT is ERC20 {
constructor() ERC20("PLOT", "PLOT") {
Expand Down
2 changes: 1 addition & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[profile.default]
src = "contracts"
src = "contracts/src"
test = "contracts/test"
script = "contracts/script"
out = "contracts/out"
Expand Down
17 changes: 4 additions & 13 deletions lib/airdrop/award.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,5 @@
export async function awardWritePoints(
_writerAddress: string,
_storylineId: number,
_timestamp?: Date,
): Promise<void> {
return;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export async function awardWritePoints(writerAddress: string, storylineId: number, timestamp?: Date): Promise<void> {}

export async function awardRatePoints(
_raterAddress: string,
_storylineId: number,
): Promise<void> {
return;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export async function awardRatePoints(raterAddress: string, storylineId: number): Promise<void> {}
2 changes: 1 addition & 1 deletion lib/airdrop/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ function getSiweCommon() {
SIWE_STATEMENT: "PlotLink Buy-Back Sprint activation" as const,
SIWE_CHAIN_ID: 8453 as const,
PLOTLINK_X_HANDLE: "plotlinkxyz" as const,
PLOTLINK_FC_FID: Number(process.env.NEXT_PUBLIC_PLOTLINK_FC_FID) || 0,
PLOTLINK_FC_FID: Number(process.env.PLOTLINK_FC_FID) || 0,
};
}

Expand Down
6 changes: 2 additions & 4 deletions lib/airdrop/points.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ import { getStreakBoost } from "./streak";

export { getStreakBoost };

export function computeBuyPoints(
plotSpent: number,
_currentStreak: number,
): number {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export function computeBuyPoints(plotSpent: number, currentStreak: number): number {
return plotSpent;
}
19 changes: 19 additions & 0 deletions lib/airdrop/siwe-verify.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,25 @@ describe("verifySiweRequest", () => {
if (!result.ok) expect(result.error).toBe("expired");
});

it("rejects message without issuedAt", async () => {
const { verifySiweRequest } = await import("./siwe-verify");
const rawMsg = [
"plotlink.xyz wants you to sign in with your Ethereum account:",
account.address,
"",
"PlotLink Buy-Back Sprint activation",
"",
"URI: https://plotlink.xyz/airdrop",
"Version: 1",
"Chain ID: 8453",
"Nonce: abcd1234",
].join("\n");
const sig = await account.signMessage({ message: rawMsg });
const result = await verifySiweRequest(rawMsg, sig);
expect(result.ok).toBe(false);
if (!result.ok) expect(["missing_issued_at", "invalid_message"]).toContain(result.error);
});

it("rejects unparseable message", async () => {
const { verifySiweRequest } = await import("./siwe-verify");
const result = await verifySiweRequest("not a siwe message", "0x1234");
Expand Down
22 changes: 12 additions & 10 deletions lib/airdrop/siwe-verify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,18 @@ export async function verifySiweRequest(
return { ok: false, error: "statement_mismatch" };
}

if (parsed.issuedAt) {
const issuedAt = new Date(parsed.issuedAt);
const now = new Date();
const ageMin = (now.getTime() - issuedAt.getTime()) / 60_000;
if (ageMin > config.SIGNATURE_FRESHNESS_MIN) {
return { ok: false, error: "expired" };
}
if (ageMin < -1) {
return { ok: false, error: "issued_in_future" };
}
if (!parsed.issuedAt) {
return { ok: false, error: "missing_issued_at" };
}

const issuedAt = new Date(parsed.issuedAt);
const now = new Date();
const ageMin = (now.getTime() - issuedAt.getTime()) / 60_000;
if (ageMin > config.SIGNATURE_FRESHNESS_MIN) {
return { ok: false, error: "expired" };
}
if (ageMin < -1) {
return { ok: false, error: "issued_in_future" };
}

try {
Expand Down
2 changes: 1 addition & 1 deletion lib/airdrop/sql.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ beforeAll(async () => {
);
`);
const migrationSql = await import("fs").then(fs =>
fs.readFileSync(new URL("../../supabase/migrations/00040_weighted_spend_function.sql", import.meta.url), "utf-8")
fs.readFileSync(new URL("../../supabase/migrations/00041_weighted_spend_function.sql", import.meta.url), "utf-8")
);
await db.exec(migrationSql.replace(/GRANT[^;]*;/, ""));
});
Expand Down
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.40.4",
"version": "1.40.5",
"private": true,
"workspaces": [
"packages/*"
Expand Down
11 changes: 3 additions & 8 deletions src/app/api/airdrop/confirm-x-handle/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,17 +79,12 @@ describe("POST /api/airdrop/confirm-x-handle", () => {
expect(res.status).toBe(409);
});

it("R16: writes pending row when twitterapi.io throws", async () => {
it("R16: returns 503 when twitterapi.io throws (no stuck state)", async () => {
vi.mocked(verifySiweRequest).mockResolvedValue({ ok: true, address: "0xabc" });
vi.mocked(lookupXUser).mockRejectedValue(new Error("network error"));

const res = await POST(makeReq({ message: "m", signature: "s", username: "downuser" }));
expect(res.status).toBe(200);
expect(mockUpsert).toHaveBeenCalledWith(
expect.objectContaining({ x_handle_confirmed_at: null, x_user_id: null }),
expect.anything(),
);
const data = await res.json();
expect(data.confirmed).toBe(false);
expect(res.status).toBe(503);
expect(mockUpsert).not.toHaveBeenCalled();
});
});
5 changes: 4 additions & 1 deletion src/app/api/airdrop/confirm-x-handle/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@ export async function POST(req: Request) {
xUserId = user.x_user_id;
confirmedAt = new Date().toISOString();
} catch {
// R16 graceful degrade: twitterapi.io down → pending row without UNIQUE lock
return NextResponse.json(
{ error: "X verification temporarily unavailable. Please retry shortly." },
{ status: 503 },
);
}

const { error: upsertErr } = await supabase
Expand Down
9 changes: 5 additions & 4 deletions src/app/api/cron/airdrop-points/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
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 { getAirdropConfig } from "../../../../../lib/airdrop/config";
import { computeBuyPoints } from "../../../../../lib/airdrop/points";

function verifyCron(req: Request): boolean {
Expand All @@ -27,8 +27,9 @@ async function handler(req: Request) {
return NextResponse.json({ error: "Supabase not configured" }, { status: 500 });
}

const config = getAirdropConfig(new Date());
const now = new Date();
if (now > AIRDROP_CONFIG.CAMPAIGN_END) {
if (now > config.CAMPAIGN_END) {
return NextResponse.json({ message: "Campaign ended, no points awarded" });
}

Expand All @@ -38,8 +39,8 @@ async function handler(req: Request) {
.from("trade_history")
.select("id, user_address, reserve_amount, block_timestamp")
.eq("event_type", "mint")
.gte("block_timestamp", AIRDROP_CONFIG.CAMPAIGN_START.toISOString())
.lte("block_timestamp", AIRDROP_CONFIG.CAMPAIGN_END.toISOString())
.gte("block_timestamp", config.CAMPAIGN_START.toISOString())
.lte("block_timestamp", config.CAMPAIGN_END.toISOString())
.not("user_address", "is", null);

if (tradesErr) {
Expand Down
2 changes: 1 addition & 1 deletion src/app/api/index/donation/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";
import { publicClient } from "../../../../../lib/rpc";
import { createServerClient } from "../../../../../lib/supabase";
import { validateRecentTx } from "../../../../../lib/index-auth";
import {
Expand Down
2 changes: 1 addition & 1 deletion src/app/api/index/plot/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";
import { publicClient } from "../../../../../lib/rpc";
import { createServerClient } from "../../../../../lib/supabase";
import { validateRecentTx } from "../../../../../lib/index-auth";
import {
Expand Down
2 changes: 1 addition & 1 deletion 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";
import { publicClient } from "../../../../../lib/rpc";
import { createServerClient } from "../../../../../lib/supabase";
import { validateRecentTx } from "../../../../../lib/index-auth";
import {
Expand Down
2 changes: 1 addition & 1 deletion src/app/api/index/trade/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, formatUnits } from "viem";
import { publicClient, getReceiptWithRetry } from "../../../../../lib/rpc";
import { publicClient } from "../../../../../lib/rpc";
import { createServerClient } from "../../../../../lib/supabase";
import { mcv2BondEventAbi } from "../../../../../lib/contracts/abi";
import { MCV2_BOND, ZAP_PLOTLINK } from "../../../../../lib/contracts/constants";
Expand Down
2 changes: 1 addition & 1 deletion src/app/api/user/link-agent/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from "next/server";
import { verifyMessage, type Address } from "viem";
import { verifyMessage } from "viem";
import { createServiceRoleClient } from "../../../../../lib/supabase";
import { publicClient } from "../../../../../lib/rpc";
import { erc8004Abi, fetchTokenOrAgentURI, resolveAgentURI } from "../../../../../lib/contracts/erc8004";
Expand Down
26 changes: 11 additions & 15 deletions src/app/profile/[address]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"use client";
/* eslint-disable @typescript-eslint/no-unused-vars, @next/next/no-img-element, react-hooks/set-state-in-effect */

import { useState, useCallback, useEffect } from "react";
import { useParams, useSearchParams } from "next/navigation";
Expand All @@ -11,7 +12,7 @@ import { STORY_FACTORY, RESERVE_LABEL, EXPLORER_URL, MCV2_BOND, PLOT_TOKEN } fro
import { getFullUserProfile, getFarcasterProfile } from "../../../../lib/actions";
import { truncateAddress } from "../../../../lib/utils";
import { formatPrice, formatSupply } from "../../../../lib/format";
import { getTokenPrice, mcv2BondAbi, erc20Abi, type TokenPriceInfo, get24hPriceChange, getTokenTVL } from "../../../../lib/price";
import { getTokenPrice, mcv2BondAbi, erc20Abi, get24hPriceChange, getTokenTVL } from "../../../../lib/price";
import { browserClient } from "../../../../lib/rpc";
import type { FarcasterProfile } from "../../../../lib/farcaster";
import type { AgentMetadata } from "../../../../lib/contracts/erc8004";
Expand All @@ -20,7 +21,7 @@ import { useNsfwPreference } from "../../../hooks/useNsfwPreference";
import { formatUsdValue } from "../../../../lib/usd-price";
import { DisconnectButton } from "../../../components/ConnectWallet";
import { GENRES, LANGUAGES } from "../../../../lib/genres";
import { DeadlineCountdown, DEADLINE_MS } from "../../../components/DeadlineCountdown";
import { DEADLINE_MS } from "../../../components/DeadlineCountdown";
import { ClaimRoyalties } from "../../../components/ClaimRoyalties";
import { WriterTradingStats } from "../../../components/WriterTradingStats";
import { DropdownSelect } from "../../../components/DropdownSelect";
Expand Down Expand Up @@ -270,7 +271,6 @@ function ProfileHeader({
<div className="flex items-start gap-4">
{/* For AI agents, use owner's PFP; otherwise use own Farcaster PFP */}
{(hasOwner && ownerFcProfile?.pfpUrl) || fcProfile?.pfpUrl ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={(hasOwner && ownerFcProfile?.pfpUrl) ? ownerFcProfile.pfpUrl : fcProfile!.pfpUrl!}
alt=""
Expand Down Expand Up @@ -468,7 +468,6 @@ function ProfileHeader({
<span className="text-muted text-[10px] font-medium uppercase tracking-wider">Operated by</span>
<div className="mt-1 flex items-center gap-2">
{ownerFcProfile?.pfpUrl && (
// eslint-disable-next-line @next/next/no-img-element
<img src={ownerFcProfile.pfpUrl} alt="" width={20} height={20} className="rounded-full" />
)}
<Link href={`/profile/${ownerAddress}`} className="text-accent hover:underline text-xs font-medium">
Expand Down Expand Up @@ -838,7 +837,7 @@ function StoriesTab({
});

// Claimable royalties (own profile only)
const { data: royaltyInfo } = useQuery({
const { data: _royaltyInfo } = useQuery({
queryKey: ["profile-royalties", address],
queryFn: async () => {
const [balance, claimed] = await browserClient.readContract({
Expand Down Expand Up @@ -1045,18 +1044,15 @@ function StoryRow({
const [isExpired, setIsExpired] = useState(false);
useEffect(() => {
if (storyline.sunset || !storyline.has_deadline || !storyline.last_plot_time) {
// eslint-disable-next-line react-hooks/set-state-in-effect -- reset when props change (e.g. deadline extension)
setIsExpired(false);
return;
}
const expiryTime = new Date(storyline.last_plot_time).getTime() + DEADLINE_MS;
const remaining = expiryTime - Date.now();
if (remaining <= 0) {
// eslint-disable-next-line react-hooks/set-state-in-effect -- one-time sync for already-expired storylines
setIsExpired(true);
return;
}
// eslint-disable-next-line react-hooks/set-state-in-effect -- reset in case props changed from expired to active
setIsExpired(false);
const timeout = setTimeout(() => setIsExpired(true), remaining);
return () => clearTimeout(timeout);
Expand Down Expand Up @@ -1536,9 +1532,9 @@ function PortfolioTab({ address, isOwnProfile }: { address: string; isOwnProfile
const {
data: donationPages,
isLoading: donGivenLoading,
isFetchingNextPage: donFetchingNext,
fetchNextPage: donFetchNext,
hasNextPage: donHasNext,
isFetchingNextPage: _donFetchingNext,
fetchNextPage: _donFetchNext,
hasNextPage: _donHasNext,
} = useInfiniteQuery({
queryKey: ["profile-donations-given", address],
queryFn: async ({ pageParam = 0 }) => {
Expand All @@ -1562,7 +1558,7 @@ function PortfolioTab({ address, isOwnProfile }: { address: string; isOwnProfile
enabled: isOwnProfile,
});
const donationsGiven = donationPages?.pages.flatMap((p) => p.rows) ?? [];
const donationTotalCount = donationPages?.pages[0]?.totalCount ?? 0;
const _donationTotalCount = donationPages?.pages[0]?.totalCount ?? 0;

// Aggregate donations received as writer
const { data: donationsReceived, isLoading: donRecvLoading } = useQuery({
Expand Down Expand Up @@ -1596,12 +1592,12 @@ function PortfolioTab({ address, isOwnProfile }: { address: string; isOwnProfile
if (isLoading) return <p className="text-muted mt-8 text-sm">Loading...</p>;

const hasHoldings = holdings && holdings.length > 0;
const hasDonationsGiven = donationsGiven.length > 0;
const hasDonationsReceived = donationsReceived && donationsReceived.count > 0;
const _hasDonationsGiven = donationsGiven.length > 0;
const _hasDonationsReceived = donationsReceived && donationsReceived.count > 0;

const totalValue = holdings?.reduce((sum, h) => sum + h.value, BigInt(0)) ?? BigInt(0);
const reserveDecimals = holdings && holdings.length > 0 ? holdings[0].reserveDecimals : 18;
const totalDonated = donationsGiven.reduce((sum, d) => sum + BigInt(d.amount), BigInt(0));
const _totalDonated = donationsGiven.reduce((sum, d) => sum + BigInt(d.amount), BigInt(0));

// Compute portfolio-level cost basis % change (only if all holdings have entry prices)
const portfolioCostPct = (() => {
Expand Down
1 change: 1 addition & 0 deletions src/components/CoverLightbox.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"use client";
/* eslint-disable @next/next/no-img-element */

import { useState, useEffect, useCallback, useRef } from "react";

Expand Down
1 change: 1 addition & 0 deletions src/components/PlotImageUpload.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"use client";
/* eslint-disable @next/next/no-img-element */

import { useRef, useState, useCallback } from "react";
import { useAccount, useSignMessage } from "wagmi";
Expand Down
1 change: 1 addition & 0 deletions src/components/StoryCard.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable @next/next/no-img-element */
import Link from "next/link";
import type { Storyline } from "../../lib/supabase";
import { WriterIdentityClient } from "./WriterIdentityClient";
Expand Down
Loading
Loading