You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
interfaceAirdropConfig{// ... other fieldsCLAIM_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:exportfunctiongetClaimWindowSeconds(config: AirdropConfig): number{if(config.CLAIM_WINDOW_SECONDS!==undefined)returnconfig.CLAIM_WINDOW_SECONDS;if(config.CLAIM_WINDOW_DAYS!==undefined)returnconfig.CLAIM_WINDOW_DAYS*86400;thrownewError("AirdropConfig must specify CLAIM_WINDOW_DAYS or CLAIM_WINDOW_SECONDS");}
Operator + orchestrator both call getClaimWindowSeconds(getAirdropConfig(new Date())) then compute claimDeadline = intendedClaimOpenTs + getClaimWindowSeconds(...).
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.
exporttypeAirdropMode="test-fast"|"test-full"|"prod";exportfunctiongetAirdropMode(): AirdropMode{constraw=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` referencefunctionbuildTestFastConfig(now: Date): AirdropConfig{return{POOL_AMOUNT: 10,CAMPAIGN_START: newDate(now.getTime()-60_000),// now - 1 minCAMPAIGN_END: newDate(now.getTime()+4*60_000),// now + 4 minMILESTONES: {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)};}functionbuildTestFullConfig(now: Date): AirdropConfig{return{POOL_AMOUNT: 100,CAMPAIGN_START: newDate(now.getTime()-60_000),// now - 1 minCAMPAIGN_END: newDate(now.getTime()+29*60_000),// now + 29 minMILESTONES: {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 PRODexportfunctiongetAirdropConfig(now: Date=newDate()): AirdropConfig{switch(getAirdropMode()){case"test-fast": returnbuildTestFastConfig(now);case"test-full": returnbuildTestFullConfig(now);default: returnPROD_CONFIG;// static — campaign dates locked at deploy}}// Backwards-compat constant — ONLY safe for PROD. Test consumers MUST call `getAirdropConfig()`.exportconstAIRDROP_CONFIG: AirdropConfig=getAirdropMode()==="prod" ? PROD_CONFIG : getAirdropConfig(newDate());
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
Epic: #1229 · Spec: §6.2, §6.5 · Type: CODE · Estimate: 0.4 day · Depends on: T0.1
Scope
Rewrite
lib/airdrop/config.tsper §6.2 + §6.5.Three modes (RE1 round-12: added
test-fullfor 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)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.
Operator + orchestrator both call
getClaimWindowSeconds(getAirdropConfig(new Date()))then computeclaimDeadline = intendedClaimOpenTs + getClaimWindowSeconds(...).PROD_CONFIG
POOL_AMOUNT: 200_000CAMPAIGN_START/CAMPAIGN_END(from T0.1, 3-month window)MILESTONES: Bronze$100K/ Silver$1M/ Gold$5M/ Diamond$10MMIN_REFERRAL_THRESHOLD: 50REFERRAL_MULTIPLIER_PER_REF: 0.2REFERRAL_MULTIPLIER_CAP: 3.0SIGNATURE_FRESHNESS_MIN: 10SIWE_DOMAIN: "plotlink.xyz",SIWE_URI: "https://plotlink.xyz/airdrop",SIWE_STATEMENT: "PlotLink Buy-Back Sprint activation",SIWE_CHAIN_ID: 8453PLOTLINK_X_HANDLE: "plotlinkxyz",PLOTLINK_FC_FID: <from T0.1>CLAIM_WINDOW_DAYS: 30(no SECONDS field)STREAK_BOOSTS = {}TEST_FAST_CONFIG (5-min smoke)
POOL_AMOUNT: 10MILESTONES: $1 / $10 / $100 / $1000 (Bronze auto-pass since real FDV >> $1)MIN_REFERRAL_THRESHOLD: 1CLAIM_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)TEST_FULL_CONFIG (30-min operator multi-wallet)
POOL_AMOUNT: 100MILESTONES: $1 / $10 / $100 / $1000MIN_REFERRAL_THRESHOLD: 5CLAIM_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)Runtime mode selection helper (RE1 round-23: dynamic for test modes)
now()forCAMPAIGN_START/END. Module-levelconstwould freeze at first import → secondtest-fullrun starts from the original timestamp → campaign already "ended" → tests fail.Caller rules:
getAirdropConfig(new Date())at every entry point — never cache.getAirdropConfig()per request.AIRDROP_CONFIGconst 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 togetAirdropConfig(new Date())— T2.4b/T2.6/T4.0 acceptance items below cover this.Acceptance
CLAIM_WINDOW_DAYS: 30, noCLAIM_WINDOW_SECONDSCLAIM_WINDOW_SECONDS: 60/180, noCLAIM_WINDOW_DAYSgetClaimWindowSeconds(config)returns correct seconds for all 3 configsgetClaimWindowSeconds()throws if neither field setgetAirdropConfig(now)builds test configs lazily — second call with a laternowreturns a fresh window (no frozen-at-import bug). Unit test: call twice withnow5 minutes apart → both configs'CAMPAIGN_ENDreflect their respectivenow + 29min.CLAIM_WINDOW_DAYSvalue anywhereSTREAK_BOOSTS = {}confirmedgetAirdropMode()defaults to"prod"on unknown/unset envpl_activationstable; downstream Phase 2 endpoints (T2.5a-f) need typed.from("pl_activations")calls.npx supabase gen types typescript --project-id <ref> > lib/supabase-types.ts(or similar; updatelib/supabase.tsDatabase type)Database["public"]["Tables"]["pl_activations"]["Row"]exists with all 11 columns from the migrationnpm run typecheck(ortsc --noEmit) passes on full repo — nopl_activationsreferences errorCLAIM_WINDOW_DAYS?+CLAIM_WINDOW_SECONDS?both optional, helper enforces at-least-one)Dependencies
T0.1 (operator config values for PROD), T0.2 (#1231 —
pl_activationsmigration applied; required before Supabase type regeneration)