{"version":3,"file":"airdrop-service.mjs","names":[],"sources":["../../../src/services/airdrop-service.ts"],"sourcesContent":["/**\n * Airdrop Service — check eligibility and generate claim calldata.\n *\n * Layered approach:\n * 1. Known airdrop contracts with hardcoded merkle root endpoints\n * 2. On-chain eligibility checks via direct contract calls\n * 3. PinchTab (browser tool) fallback for UI-based claims\n *\n * No single API covers all airdrops. This service maintains a registry\n * of known active/recent airdrops and their claim mechanisms.\n *\n * No external dependencies — uses viem for contract reads and\n * guardedFetch for any HTTP endpoints.\n */\n\nimport { guardedFetch } from './endpoint-allowlist.js';\nimport { encodeFunctionData, type Address } from 'viem';\n\n// ── Types ────────────────────────────────────────────────────────────────\n\nexport interface AirdropInfo {\n  id: string;\n  name: string;\n  token: string;\n  tokenSymbol: string;\n  chain: string;\n  chainId: number;\n  status: 'active' | 'ended' | 'upcoming';\n  claimContract: string;\n  /** URL to check eligibility (REST API or dApp) */\n  eligibilityUrl?: string;\n  /** If true, claim requires browser automation (no direct contract call) */\n  requiresBrowser?: boolean;\n  deadline?: string;\n  description: string;\n}\n\nexport interface EligibilityResult {\n  airdropId: string;\n  airdropName: string;\n  eligible: boolean;\n  amount?: string;\n  amountFormatted?: string;\n  proof?: string[];\n  claimIndex?: number;\n  claimed?: boolean;\n  error?: string;\n}\n\nexport interface ClaimCalldata {\n  to: string;\n  data: string;\n  value: string;\n  description: string;\n}\n\n// ── Known Airdrops Registry ─────────────────────────────────────────────\n// Maintained manually. Each entry describes a known airdrop with its\n// claim mechanism. Active airdrops are checked first.\n\nconst KNOWN_AIRDROPS: AirdropInfo[] = [\n  // ── Active / Recent ───────────────────────────────────────────────\n  {\n    id: 'eigen-s2',\n    name: 'EigenLayer Season 2',\n    token: '0xec53bF9167f50cDEB3Ae105f56099aaaB9061F83',\n    tokenSymbol: 'EIGEN',\n    chain: 'ethereum',\n    chainId: 1,\n    status: 'active',\n    claimContract: '0x035bdA26Bf4d270CfdBe9b32F3580C76BbDdE1F9',\n    eligibilityUrl: 'https://claims.eigenfoundation.org/clique-eigenlayer-s2/check',\n    description: 'EigenLayer Season 2 stakedrop for restakers and operators.',\n  },\n  {\n    id: 'zk-nation',\n    name: 'ZKsync (ZK Nation)',\n    token: '0x5A7d6b2F92C77FAD6CCaBd7EE0624E64907Eaf3E',\n    tokenSymbol: 'ZK',\n    chain: 'ethereum',\n    chainId: 1,\n    status: 'active',\n    claimContract: '0x66Fd4FC8FA52c9bec2AbA368047A0b27e24ecfe4',\n    description: 'ZKsync governance token airdrop for ecosystem participants.',\n    requiresBrowser: true,\n  },\n  {\n    id: 'layerzero',\n    name: 'LayerZero (ZRO)',\n    token: '0x6985884C4392D348587B19cb9eAAf157F13271cd',\n    tokenSymbol: 'ZRO',\n    chain: 'ethereum',\n    chainId: 1,\n    status: 'active',\n    claimContract: '0xB09F16F625B363875e39ADa56C03682c4B8C01C9',\n    eligibilityUrl: 'https://www.layerzero.foundation/eligibility',\n    description: 'LayerZero protocol token for cross-chain messaging users.',\n    requiresBrowser: true,\n  },\n  {\n    id: 'scroll',\n    name: 'Scroll (SCR)',\n    token: '0xd29687c813D741E2F938F4aC377128810E217b1b',\n    tokenSymbol: 'SCR',\n    chain: 'ethereum',\n    chainId: 1,\n    status: 'active',\n    claimContract: '0xA6EA2f3299b63c53143c993d2d5E60A69CD139Ed',\n    description: 'Scroll zkEVM L2 token airdrop for bridge and ecosystem users.',\n    requiresBrowser: true,\n  },\n\n  // ── Base Ecosystem ────────────────────────────────────────────────\n  {\n    id: 'degen-s2',\n    name: 'Degen Chain Season 2',\n    token: '0x4ed4E862860beD51a9570b96d89aF5E1B0Efefed',\n    tokenSymbol: 'DEGEN',\n    chain: 'base',\n    chainId: 8453,\n    status: 'active',\n    claimContract: '0x0000000000000000000000000000000000000000',\n    description: 'DEGEN token airdrop for Farcaster tippers and Degen L3 users.',\n    requiresBrowser: true,\n  },\n  {\n    id: 'morpho',\n    name: 'Morpho (MORPHO)',\n    token: '0x58D97B57BB95320F9a05dC918Aef65434969c2B2',\n    tokenSymbol: 'MORPHO',\n    chain: 'ethereum',\n    chainId: 1,\n    status: 'active',\n    claimContract: '0x678dDC1d07eaa166E502E4eb00E6752Ec7BFc530',\n    description: 'Morpho lending protocol token for depositors and borrowers.',\n  },\n\n  // ── Ended (for history/reference) ─────────────────────────────────\n  {\n    id: 'arb-dao',\n    name: 'Arbitrum DAO (ARB)',\n    token: '0x912CE59144191C1204E64559FE8253a0e49E6548',\n    tokenSymbol: 'ARB',\n    chain: 'arbitrum',\n    chainId: 42161,\n    status: 'ended',\n    claimContract: '0x67a24CE4321aB3aF51c2D0a4801c3E111D88C9d9',\n    description: 'Arbitrum governance token airdrop (March 2023). Claim period ended.',\n  },\n  {\n    id: 'op-s4',\n    name: 'Optimism Season 4 (OP)',\n    token: '0x4200000000000000000000000000000000000042',\n    tokenSymbol: 'OP',\n    chain: 'optimism',\n    chainId: 10,\n    status: 'ended',\n    claimContract: '0xFeDFAF1A10335448b7FA0268F56D2B44DBD357de',\n    description: 'Optimism governance token Season 4 airdrop. Claim period ended.',\n  },\n];\n\n// ── Standard Merkle Distributor ABI (minimal) ───────────────────────────\n// Most airdrops use a Merkle distributor pattern. We only need these functions.\n\nconst MERKLE_CLAIM_ABI = [\n  {\n    name: 'isClaimed',\n    type: 'function',\n    stateMutability: 'view',\n    inputs: [{ name: 'index', type: 'uint256' }],\n    outputs: [{ name: '', type: 'bool' }],\n  },\n  {\n    name: 'claim',\n    type: 'function',\n    stateMutability: 'nonpayable',\n    inputs: [\n      { name: 'index', type: 'uint256' },\n      { name: 'account', type: 'address' },\n      { name: 'amount', type: 'uint256' },\n      { name: 'merkleProof', type: 'bytes32[]' },\n    ],\n    outputs: [],\n  },\n] as const;\n\n// ── Service ──────────────────────────────────────────────────────────────\n\nexport class AirdropService {\n  /**\n   * List known airdrops, optionally filtered by status or chain.\n   */\n  listAirdrops(opts?: {\n    status?: 'active' | 'ended' | 'upcoming' | 'all';\n    chain?: string;\n  }): AirdropInfo[] {\n    let results = [...KNOWN_AIRDROPS];\n\n    if (opts?.status && opts.status !== 'all') {\n      results = results.filter(a => a.status === opts.status);\n    }\n    if (opts?.chain) {\n      const chainLower = opts.chain.toLowerCase();\n      results = results.filter(a => a.chain === chainLower);\n    }\n\n    return results;\n  }\n\n  /**\n   * Check eligibility for a specific airdrop.\n   * Returns eligibility info including amount and claim status.\n   */\n  async checkEligibility(\n    airdropId: string,\n    address: string,\n    publicClient?: any,\n  ): Promise<EligibilityResult> {\n    const airdrop = KNOWN_AIRDROPS.find(a => a.id === airdropId);\n    if (!airdrop) {\n      return {\n        airdropId,\n        airdropName: 'Unknown',\n        eligible: false,\n        error: `Unknown airdrop: ${airdropId}. Use action=list to see known airdrops.`,\n      };\n    }\n\n    // Try eligibility URL if available\n    if (airdrop.eligibilityUrl && !airdrop.requiresBrowser) {\n      try {\n        return await this.checkViaApi(airdrop, address);\n      } catch {\n        // Fall through to on-chain check\n      }\n    }\n\n    // If browser required, return guidance\n    if (airdrop.requiresBrowser) {\n      return {\n        airdropId: airdrop.id,\n        airdropName: airdrop.name,\n        eligible: false, // unknown without browser\n        error: `${airdrop.name} requires browser-based eligibility check. Use the browser tool to navigate to the claim page.`,\n      };\n    }\n\n    // Try on-chain eligibility check\n    if (publicClient && airdrop.claimContract !== '0x0000000000000000000000000000000000000000') {\n      try {\n        return await this.checkOnchain(airdrop, address, publicClient);\n      } catch {\n        return {\n          airdropId: airdrop.id,\n          airdropName: airdrop.name,\n          eligible: false,\n          error: 'On-chain eligibility check failed. The contract may use a non-standard interface.',\n        };\n      }\n    }\n\n    return {\n      airdropId: airdrop.id,\n      airdropName: airdrop.name,\n      eligible: false,\n      error: 'Could not determine eligibility. Check the project\\'s claim page directly.',\n    };\n  }\n\n  /**\n   * Check eligibility for ALL active airdrops.\n   */\n  async checkAll(\n    address: string,\n    publicClient?: any,\n  ): Promise<EligibilityResult[]> {\n    const active = this.listAirdrops({ status: 'active' });\n    const results: EligibilityResult[] = [];\n\n    for (const airdrop of active) {\n      const result = await this.checkEligibility(airdrop.id, address, publicClient);\n      results.push(result);\n    }\n\n    return results;\n  }\n\n  /**\n   * Generate claim calldata for a Merkle distributor airdrop.\n   * Requires the merkle proof and claim index (from eligibility check).\n   */\n  generateClaimCalldata(\n    airdropId: string,\n    claimIndex: number,\n    address: string,\n    amount: string,\n    proof: string[],\n  ): ClaimCalldata | null {\n    const airdrop = KNOWN_AIRDROPS.find(a => a.id === airdropId);\n    if (!airdrop) return null;\n    if (airdrop.claimContract === '0x0000000000000000000000000000000000000000') return null;\n\n    // Validate inputs before BigInt conversion to prevent opaque errors\n    if (!Number.isInteger(claimIndex) || claimIndex < 0) return null;\n    if (!/^\\d+$/.test(amount)) return null;\n\n    const data = encodeFunctionData({\n      abi: MERKLE_CLAIM_ABI,\n      functionName: 'claim',\n      args: [\n        BigInt(claimIndex),\n        address as Address,\n        BigInt(amount),\n        proof as `0x${string}`[],\n      ],\n    });\n\n    return {\n      to: airdrop.claimContract,\n      data,\n      value: '0',\n      description: `Claim ${airdrop.tokenSymbol} from ${airdrop.name}`,\n    };\n  }\n\n  /**\n   * Get a specific airdrop by ID.\n   */\n  getAirdrop(id: string): AirdropInfo | undefined {\n    return KNOWN_AIRDROPS.find(a => a.id === id);\n  }\n\n  // ── Internal ──────────────────────────────────────────────────────\n\n  private async checkViaApi(\n    airdrop: AirdropInfo,\n    address: string,\n  ): Promise<EligibilityResult> {\n    const url = `${airdrop.eligibilityUrl}?address=${address.toLowerCase()}`;\n    const response = await guardedFetch(url, {\n      headers: { accept: 'application/json' },\n      signal: AbortSignal.timeout(15_000),\n    });\n\n    if (!response.ok) {\n      throw new Error(`API ${response.status}`);\n    }\n\n    const data = await response.json() as any;\n\n    // Most eligibility APIs return { eligible, amount, proof, index }\n    return {\n      airdropId: airdrop.id,\n      airdropName: airdrop.name,\n      eligible: !!data.eligible || !!data.amount || (data.amount && data.amount !== '0'),\n      amount: data.amount ?? data.claimableAmount ?? undefined,\n      amountFormatted: data.amountFormatted ?? undefined,\n      proof: data.proof ?? data.merkleProof ?? undefined,\n      claimIndex: data.index ?? data.claimIndex ?? undefined,\n      claimed: data.claimed ?? data.hasClaimed ?? undefined,\n    };\n  }\n\n  private async checkOnchain(\n    airdrop: AirdropInfo,\n    _address: string,\n    _publicClient: any,\n  ): Promise<EligibilityResult> {\n    // On-chain checks are airdrop-specific. Without a merkle proof index,\n    // we can only check if the contract exists and is active.\n    // Full on-chain eligibility requires the merkle tree data.\n    return {\n      airdropId: airdrop.id,\n      airdropName: airdrop.name,\n      eligible: false,\n      error: 'On-chain eligibility requires merkle proof data. Check the claim page or use the browser tool.',\n    };\n  }\n}\n\n// ── Singleton ────────────────────────────────────────────────────────────\n\nlet _instance: AirdropService | null = null;\n\nexport function getAirdropService(): AirdropService {\n  if (!_instance) _instance = new AirdropService();\n  return _instance;\n}\n\nexport function resetAirdropService(): void {\n  _instance = null;\n}\n"],"mappings":"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"}