{"version":3,"file":"permit2.mjs","names":[],"sources":["../../../src/tools/permit2.ts"],"sourcesContent":["/**\n * Permit2 Tool — Manage Uniswap Permit2 token allowances.\n *\n * Permit2 is the universal token approval system used by Uniswap V4 and other\n * modern DeFi protocols. Instead of approving each contract individually,\n * you approve Permit2 once per token, then use gas-free signatures for\n * individual spender allowances.\n *\n * Actions:\n *   check_allowance — Read current Permit2 allowance for a token/spender pair\n *   approve         — Approve a token for Permit2 (ERC-20 → Permit2 max approval)\n *   approve_batch   — Approve multiple tokens for Permit2 in sequence\n *   revoke          — Set a Permit2 allowance to zero for a specific spender\n *   lockdown        — Emergency: revoke all Permit2 allowances for multiple pairs\n *\n * Uses Permit2Client from @clawnch/clawncher-sdk.\n */\n\nimport { Type } from '@sinclair/typebox';\nimport { stringEnum, jsonResult, errorResult, readStringParam } from '../lib/tool-helpers.js';\nimport {\n  getWalletState,\n  requireWalletClient,\n  requirePublicClient,\n  isBankrMode,\n} from '../services/walletconnect-service.js';\n\nconst ACTIONS = ['check_allowance', 'approve', 'approve_batch', 'revoke', 'lockdown'] as const;\n\n/** Canonical Permit2 address (same on all chains) */\nconst PERMIT2_ADDRESS = '0x000000000022D473030F116dDEE9F6B43aC78BA3';\n\n/** Well-known spender addresses on Base */\nconst KNOWN_SPENDERS: Record<string, string> = {\n  universal_router: '0x198EF79F1F515F02dFE9e3115eD9fC07183f02fC',\n  position_manager: '0x7C5f5A4bBd8fD63184577525326123B519429bDc',\n};\n\nconst Permit2Schema = Type.Object({\n  action: stringEnum(ACTIONS, {\n    description:\n      'check_allowance: read current Permit2 allowance for token/spender. ' +\n      'approve: approve a token for Permit2 (one-time ERC-20 max approval). ' +\n      'approve_batch: approve multiple tokens for Permit2. ' +\n      'revoke: set Permit2 allowance to zero for a spender. ' +\n      'lockdown: emergency revoke multiple token/spender pairs.',\n  }),\n  token: Type.Optional(Type.String({\n    description: 'ERC-20 token contract address (0x...). Required for check_allowance, approve, revoke.',\n  })),\n  amount: Type.Optional(Type.String({\n    description: 'Scoped approval amount in token smallest unit. If omitted, max approval is used. Use to limit Permit2 exposure.',\n  })),\n  tokens: Type.Optional(Type.Array(Type.String(), {\n    description: 'Array of token addresses for approve_batch.',\n  })),\n  spender: Type.Optional(Type.String({\n    description:\n      'Spender address (0x...) or alias: \"universal_router\", \"position_manager\". ' +\n      'Required for check_allowance, revoke.',\n  })),\n  pairs: Type.Optional(Type.Array(\n    Type.Object({\n      token: Type.String({ description: 'Token address' }),\n      spender: Type.String({ description: 'Spender address or alias' }),\n    }),\n    { description: 'Token/spender pairs for lockdown action.' },\n  )),\n});\n\nexport function createPermit2Tool() {\n  return {\n    name: 'permit2',\n    label: 'Permit2',\n    ownerOnly: true,\n    description:\n      'Manage Uniswap Permit2 token allowances. Check, approve, or revoke Permit2 ' +\n      'allowances for DeFi protocols. Use \"lockdown\" for emergency revocation.',\n    parameters: Permit2Schema,\n    execute: async (_toolCallId: string, args: unknown) => {\n      const params = args as Record<string, unknown>;\n      const action = readStringParam(params, 'action', { required: true })!;\n\n      const state = getWalletState();\n      if (!state.connected) {\n        return errorResult('No wallet connected. Use clawnchconnect tool to connect first.');\n      }\n\n      // Bankr mode: Permit2 operations are handled differently\n      // Approvals go through Bankr prompt API, not local wallet\n      if (isBankrMode() && (action === 'approve' || action === 'approve_batch')) {\n        return jsonResult({\n          status: 'not_needed',\n          mode: 'bankr',\n          message: 'Bankr wallet handles token approvals automatically during swaps. ' +\n            'No manual Permit2 approval is needed.',\n        });\n      }\n\n      switch (action) {\n        case 'check_allowance':\n          return handleCheckAllowance(params);\n        case 'approve':\n          return handleApprove(params);\n        case 'approve_batch':\n          return handleApproveBatch(params);\n        case 'revoke':\n          return handleRevoke(params);\n        case 'lockdown':\n          return handleLockdown(params);\n        default:\n          return errorResult(`Unknown action: ${action}. Use: ${ACTIONS.join(', ')}`);\n      }\n    },\n  };\n}\n\n// ─── Helpers ──────────────────────────────────────────────────────────────\n\nfunction resolveSpender(input: string): `0x${string}` {\n  const alias = KNOWN_SPENDERS[input.toLowerCase()];\n  if (alias) return alias as `0x${string}`;\n  if (input.startsWith('0x') && input.length === 42) return input as `0x${string}`;\n  throw new Error(`Invalid spender: \"${input}\". Use an address (0x...) or alias: ${Object.keys(KNOWN_SPENDERS).join(', ')}`);\n}\n\nasync function getPermit2Client() {\n  const { Permit2Client } = await import('@clawnch/clawncher-sdk');\n  const wallet = requireWalletClient();\n  const publicClient = requirePublicClient();\n\n  return new Permit2Client({\n    wallet,\n    publicClient,\n    chainId: 8453,\n  });\n}\n\n// ─── Action Handlers ──────────────────────────────────────────────────────\n\nasync function handleCheckAllowance(params: Record<string, unknown>) {\n  const token = readStringParam(params, 'token', { required: true })!;\n  const spenderInput = readStringParam(params, 'spender', { required: true })!;\n\n  try {\n    const spender = resolveSpender(spenderInput);\n    const client = await getPermit2Client();\n    const allowance = await client.getAllowance(token as `0x${string}`, spender);\n\n    const now = Math.floor(Date.now() / 1000);\n    const isExpired = allowance.expiration > 0 && allowance.expiration < now;\n    const isActive = allowance.amount > 0n && !isExpired;\n\n    return jsonResult({\n      token,\n      spender,\n      spenderAlias: KNOWN_SPENDERS[spenderInput.toLowerCase()] ? spenderInput : undefined,\n      amount: allowance.amount.toString(),\n      amountReadable: allowance.amount === BigInt('0xffffffffffffffffffffffffffffffffffffffff')\n        ? 'MAX (uint160)'\n        : allowance.amount.toString(),\n      expiration: allowance.expiration,\n      expirationDate: allowance.expiration > 0\n        ? new Date(allowance.expiration * 1000).toISOString()\n        : 'never',\n      nonce: allowance.nonce,\n      isExpired,\n      isActive,\n      permit2Address: PERMIT2_ADDRESS,\n    });\n  } catch (err) {\n    return errorResult(`Check allowance failed: ${err instanceof Error ? err.message : String(err)}`);\n  }\n}\n\nasync function handleApprove(params: Record<string, unknown>) {\n  const token = readStringParam(params, 'token', { required: true })!;\n\n  // C4: Validate token address\n  if (!token.match(/^0x[a-fA-F0-9]{40}$/)) {\n    return errorResult('Invalid token address. Must be a valid 0x... address.');\n  }\n\n  try {\n    const client = await getPermit2Client();\n\n    // C4: Support scoped approvals — if caller specifies an amount, use directApprove\n    // with that amount instead of max uint160. This limits exposure.\n    const scopedAmount = readStringParam(params, 'amount');\n    if (scopedAmount) {\n      // Validate amount before BigInt conversion — must be a non-negative integer (smallest unit)\n      if (!/^\\d+$/.test(scopedAmount.trim())) {\n        return errorResult(`Invalid amount: \"${scopedAmount}\". Must be a non-negative integer in the token's smallest unit (e.g. wei).`);\n      }\n      // Parse the amount as a BigInt (expected in token's smallest unit)\n      const spenderInput = readStringParam(params, 'spender') || 'universal_router';\n      const spender = resolveSpender(spenderInput);\n      const amount = BigInt(scopedAmount.trim());\n      const expiration = Math.floor(Date.now() / 1000) + 86400 * 30; // 30 days\n      const txHash = await client.directApprove(\n        token as `0x${string}`,\n        spender,\n        amount,\n        expiration,\n      );\n      return jsonResult({\n        status: 'approved',\n        token,\n        permit2Address: PERMIT2_ADDRESS,\n        txHash,\n        approvedAmount: scopedAmount,\n        expiresIn: '30 days',\n        message: 'Token approved for Permit2 with scoped allowance.',\n      });\n    }\n\n    // Default path: ensure max approval (for backward compat)\n    const txHash = await client.ensureTokenApproval(token as `0x${string}`);\n\n    if (txHash === null) {\n      return jsonResult({\n        status: 'already_approved',\n        token,\n        permit2Address: PERMIT2_ADDRESS,\n        message: 'Token already has max approval for Permit2. No transaction needed.',\n      });\n    }\n\n    return jsonResult({\n      status: 'approved',\n      token,\n      permit2Address: PERMIT2_ADDRESS,\n      txHash,\n      message: 'Token approved for Permit2 with max allowance. Use \"amount\" param to scope the approval.',\n    });\n  } catch (err) {\n    return errorResult(`Approve failed: ${err instanceof Error ? err.message : String(err)}`);\n  }\n}\n\nasync function handleApproveBatch(params: Record<string, unknown>) {\n  const tokensRaw = params.tokens;\n  if (!Array.isArray(tokensRaw) || tokensRaw.length === 0) {\n    return errorResult('approve_batch requires a non-empty \"tokens\" array of token addresses.');\n  }\n\n  const tokens: `0x${string}`[] = [];\n  for (const t of tokensRaw) {\n    const addr = String(t).toLowerCase();\n    if (!/^0x[0-9a-f]{40}$/.test(addr)) {\n      return errorResult(`Invalid token address: \"${t}\". Must be a 42-character hex address starting with 0x.`);\n    }\n    tokens.push(addr as `0x${string}`);\n  }\n\n  try {\n    const client = await getPermit2Client();\n    const results = await client.ensureTokenApprovals(tokens);\n\n    const approvals: Array<{ token: string; txHash: string | null; status: string }> = [];\n    for (const token of tokens) {\n      const hash = results.get(token as any);\n      approvals.push({\n        token,\n        txHash: hash ?? null,\n        status: hash ? 'approved' : 'already_approved',\n      });\n    }\n\n    return jsonResult({\n      permit2Address: PERMIT2_ADDRESS,\n      totalTokens: tokens.length,\n      newApprovals: approvals.filter((a) => a.status === 'approved').length,\n      alreadyApproved: approvals.filter((a) => a.status === 'already_approved').length,\n      approvals,\n    });\n  } catch (err) {\n    return errorResult(`Batch approve failed: ${err instanceof Error ? err.message : String(err)}`);\n  }\n}\n\nasync function handleRevoke(params: Record<string, unknown>) {\n  const token = readStringParam(params, 'token', { required: true })!;\n  const spenderInput = readStringParam(params, 'spender', { required: true })!;\n\n  try {\n    const spender = resolveSpender(spenderInput);\n    const client = await getPermit2Client();\n\n    // directApprove with amount=0 and expiration=0 effectively revokes\n    const txHash = await client.directApprove(\n      token as `0x${string}`,\n      spender,\n      0n,\n      0,\n    );\n\n    return jsonResult({\n      status: 'revoked',\n      token,\n      spender,\n      txHash,\n      message: 'Permit2 allowance set to zero for this token/spender pair.',\n    });\n  } catch (err) {\n    return errorResult(`Revoke failed: ${err instanceof Error ? err.message : String(err)}`);\n  }\n}\n\nasync function handleLockdown(params: Record<string, unknown>) {\n  const pairsRaw = params.pairs;\n  if (!Array.isArray(pairsRaw) || pairsRaw.length === 0) {\n    return errorResult('lockdown requires a non-empty \"pairs\" array of {token, spender} objects.');\n  }\n\n  try {\n    const pairs: Array<{ token: `0x${string}`; spender: `0x${string}` }> = [];\n    for (const p of pairsRaw as Array<{ token?: unknown; spender?: unknown }>) {\n      const tokenAddr = String(p.token ?? '').toLowerCase();\n      if (!/^0x[0-9a-f]{40}$/.test(tokenAddr)) {\n        return errorResult(`Invalid token address in lockdown pair: \"${p.token}\".`);\n      }\n      pairs.push({\n        token: tokenAddr as `0x${string}`,\n        spender: resolveSpender(String(p.spender ?? '')),\n      });\n    }\n\n    const client = await getPermit2Client();\n    const txHash = await client.lockdown(pairs);\n\n    return jsonResult({\n      status: 'lockdown_complete',\n      pairsRevoked: pairs.length,\n      pairs: pairs.map((p) => ({ token: p.token, spender: p.spender })),\n      txHash,\n      message: `Emergency lockdown: ${pairs.length} token/spender pair(s) revoked.`,\n    });\n  } catch (err) {\n    return errorResult(`Lockdown failed: ${err instanceof Error ? err.message : String(err)}`);\n  }\n}\n"],"mappings":"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"}