Skip to content

[T2.1] Update lib/airdrop/config.ts (PROD + TEST configs) #1236

Description

@realproject7

Epic: #1229 · Spec: §6.2, §6.5 · Type: CODE · Estimate: 0.4 day · Depends on: T0.1

Scope

Rewrite lib/airdrop/config.ts per §6.2 + §6.5.

Three modes (RE1 round-12: added test-full for operator multi-wallet verification)

Selected by NEXT_PUBLIC_AIRDROP_MODE:

  • "test-fast"TEST_FAST_CONFIG — 5-min campaign, CI-friendly (used by T4.0 backend smoke + Playwright)
  • "test-full"TEST_FULL_CONFIG — 30-min campaign, operator multi-wallet orchestration (T4.1)
  • anything else → PROD_CONFIG — real 3-month campaign

🚨 Claim window — integer seconds for test modes (RE1 round-13 fix)

Previous draft used fractional CLAIM_WINDOW_DAYS ≈ 1/144 (10 min) for test-full, which conflicted with T4.0 #1268 Phase 5/6 timing (~3 min claim window needed within the 30-min budget). Fractional days also leak awkwardly into contract/deploy math.

Fix: introduce separate field for test modes.

interface AirdropConfig {
  // ... other fields
  CLAIM_WINDOW_DAYS?: number;          // PROD only — integer days (e.g., 30)
  CLAIM_WINDOW_SECONDS?: number;       // TEST modes — integer seconds
}

// Helper used by operator at T6.2 deploy + by test orchestrator:
export function getClaimWindowSeconds(config: AirdropConfig): number {
  if (config.CLAIM_WINDOW_SECONDS !== undefined) return config.CLAIM_WINDOW_SECONDS;
  if (config.CLAIM_WINDOW_DAYS !== undefined) return config.CLAIM_WINDOW_DAYS * 86400;
  throw new Error("AirdropConfig must specify CLAIM_WINDOW_DAYS or CLAIM_WINDOW_SECONDS");
}

Operator + orchestrator both call getClaimWindowSeconds(getAirdropConfig(new Date())) then compute claimDeadline = intendedClaimOpenTs + getClaimWindowSeconds(...).

PROD_CONFIG

  • POOL_AMOUNT: 200_000
  • CAMPAIGN_START / CAMPAIGN_END (from T0.1, 3-month window)
  • MILESTONES: Bronze $100K / Silver $1M / Gold $5M / Diamond $10M
  • MIN_REFERRAL_THRESHOLD: 50
  • REFERRAL_MULTIPLIER_PER_REF: 0.2
  • REFERRAL_MULTIPLIER_CAP: 3.0
  • SIGNATURE_FRESHNESS_MIN: 10
  • 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: <from T0.1>
  • CLAIM_WINDOW_DAYS: 30 (no SECONDS field)
  • STREAK_BOOSTS = {}

TEST_FAST_CONFIG (5-min smoke)

  • 5-min campaign (now-1m → now+4m)
  • POOL_AMOUNT: 10
  • MILESTONES: $1 / $10 / $100 / $1000 (Bronze auto-pass since real FDV >> $1)
  • MIN_REFERRAL_THRESHOLD: 1
  • CLAIM_WINDOW_SECONDS: 60 (1 min — claim window so small that smoke-test doesn't include claim flow; that's T4.0 Tier 2's job)
  • Same SIWE_* / PLOTLINK_FC_FID as PROD
  • Other multiplier params same as PROD

TEST_FULL_CONFIG (30-min operator multi-wallet)

  • 30-min campaign (now-1m → now+29m)
  • POOL_AMOUNT: 100
  • MILESTONES: $1 / $10 / $100 / $1000
  • MIN_REFERRAL_THRESHOLD: 5
  • CLAIM_WINDOW_SECONDS: 180 (3 min — aligns with T4.0 [T4.0] Deploy gate automation scripts (T2.x, T3.x, Pre-T4.1) #1268 Phase 5-6 timing: deploy at ~22min, claim window 22-25min, post-deadline test at 25-28min, sweep at 28min)
  • Same SIWE_* / PLOTLINK_FC_FID as TEST_FAST_CONFIG
  • Other multiplier params same as PROD

Runtime mode selection helper (RE1 round-23: dynamic for test modes)

⚠️ Test modes MUST be regenerated per call — they reference now() for CAMPAIGN_START/END. Module-level const would freeze at first import → second test-full run starts from the original timestamp → campaign already "ended" → tests fail.

export type AirdropMode = "test-fast" | "test-full" | "prod";

export function getAirdropMode(): AirdropMode {
  const raw = process.env.NEXT_PUBLIC_AIRDROP_MODE;
  if (raw === "test-fast") return "test-fast";
  if (raw === "test-full") return "test-full";
  return "prod";
}

// Build TEST configs lazily from a `now` reference
function buildTestFastConfig(now: Date): AirdropConfig {
  return {
    POOL_AMOUNT: 10,
    CAMPAIGN_START: new Date(now.getTime() - 60_000),       // now - 1 min
    CAMPAIGN_END:   new Date(now.getTime() + 4 * 60_000),   // now + 4 min
    MILESTONES: { BRONZE: { mcap: 1, pct: 10 }, SILVER: { mcap: 10, pct: 30 }, GOLD: { mcap: 100, pct: 50 }, DIAMOND: { mcap: 1000, pct: 100 } },
    MIN_REFERRAL_THRESHOLD: 1,
    CLAIM_WINDOW_SECONDS: 60,
    // ... (same SIWE/multiplier/PLOTLINK_* as PROD)
  };
}

function buildTestFullConfig(now: Date): AirdropConfig {
  return {
    POOL_AMOUNT: 100,
    CAMPAIGN_START: new Date(now.getTime() - 60_000),         // now - 1 min
    CAMPAIGN_END:   new Date(now.getTime() + 29 * 60_000),    // now + 29 min
    MILESTONES: { BRONZE: { mcap: 1, pct: 10 }, SILVER: { mcap: 10, pct: 30 }, GOLD: { mcap: 100, pct: 50 }, DIAMOND: { mcap: 1000, pct: 100 } },
    MIN_REFERRAL_THRESHOLD: 5,
    CLAIM_WINDOW_SECONDS: 180,
    // ... (same SIWE/multiplier/PLOTLINK_* as PROD)
  };
}

// Primary export: pass `now` for tests, default for PROD
export function getAirdropConfig(now: Date = new Date()): AirdropConfig {
  switch (getAirdropMode()) {
    case "test-fast": return buildTestFastConfig(now);
    case "test-full": return buildTestFullConfig(now);
    default: return PROD_CONFIG;  // static — campaign dates locked at deploy
  }
}

// Backwards-compat constant — ONLY safe for PROD. Test consumers MUST call `getAirdropConfig()`.
export const AIRDROP_CONFIG: AirdropConfig =
  getAirdropMode() === "prod" ? PROD_CONFIG : getAirdropConfig(new Date());

Caller rules:

  • T2.4b shared SQL helper, T2.6 finalize, T4.0 orchestrator: call getAirdropConfig(new Date()) at every entry point — never cache.
  • Other backend endpoints: same — call getAirdropConfig() per request.
  • Frontend: AIRDROP_CONFIG const is fine for static prod, fine for test (since UI also reloads).

Backward-compat note

Existing v1 callers using AIRDROP_CONFIG (module const) keep working in PROD (PROD is static). Test-mode callers MUST migrate to getAirdropConfig(new Date()) — T2.4b/T2.6/T4.0 acceptance items below cover this.

Acceptance

  • PROD_CONFIG: CLAIM_WINDOW_DAYS: 30, no CLAIM_WINDOW_SECONDS
  • TEST_FAST_CONFIG / TEST_FULL_CONFIG: CLAIM_WINDOW_SECONDS: 60 / 180, no CLAIM_WINDOW_DAYS
  • getClaimWindowSeconds(config) returns correct seconds for all 3 configs
  • getClaimWindowSeconds() throws if neither field set
  • getAirdropConfig(now) builds test configs lazily — second call with a later now returns a fresh window (no frozen-at-import bug). Unit test: call twice with now 5 minutes apart → both configs' CAMPAIGN_END reflect their respective now + 29min.
  • No fractional CLAIM_WINDOW_DAYS value anywhere
  • STREAK_BOOSTS = {} confirmed
  • getAirdropMode() defaults to "prod" on unknown/unset env
  • Supabase types regenerated (RE1 round-23): T0.2 added pl_activations table; downstream Phase 2 endpoints (T2.5a-f) need typed .from("pl_activations") calls.
    • Run: npx supabase gen types typescript --project-id <ref> > lib/supabase-types.ts (or similar; update lib/supabase.ts Database type)
    • Confirm Database["public"]["Tables"]["pl_activations"]["Row"] exists with all 11 columns from the migration
    • npm run typecheck (or tsc --noEmit) passes on full repo — no pl_activations references error
  • Type-check passes (CLAIM_WINDOW_DAYS? + CLAIM_WINDOW_SECONDS? both optional, helper enforces at-least-one)

Dependencies

T0.1 (operator config values for PROD), T0.2 (#1231pl_activations migration applied; required before Supabase type regeneration)

Metadata

Metadata

Assignees

No one assigned

    Labels

    airdropPLOT 10x Airdrop Campaign

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions