{"version":3,"file":"defi-stake.mjs","names":[],"sources":["../../../src/tools/defi-stake.ts"],"sourcesContent":["/**\n * DeFi Staking Tool — liquid staking operations.\n *\n * Actions:\n *   stake     — Stake ETH to receive a liquid staking token (stETH, rETH)\n *   unstake   — Burn/redeem liquid staking token back to ETH\n *   wrap      — Wrap stETH → wstETH (Lido)\n *   unwrap    — Unwrap wstETH → stETH (Lido)\n *   positions — View current staking positions and APYs\n *\n * Protocols: Lido (stETH/wstETH), Rocket Pool (rETH), Coinbase (cbETH)\n * Chain: Ethereum mainnet (positions also shown on Base for bridged LSTs)\n */\n\nimport { Type } from '@sinclair/typebox';\nimport { parseEther } from 'viem';\nimport { stringEnum, jsonResult, errorResult, readStringParam } from '../lib/tool-helpers.js';\nimport {\n  getWalletState,\n  requireWalletClient,\n  requirePublicClient,\n} from '../services/walletconnect-service.js';\nimport { getStakingService } from '../services/staking-service.js';\nimport { getRpcManager } from '../services/rpc-provider.js';\n\nconst ACTIONS = ['stake', 'unstake', 'wrap', 'unwrap', 'positions'] as const;\n\nconst DefiStakeSchema = Type.Object({\n  action: stringEnum(ACTIONS, {\n    description:\n      'stake: send ETH to receive stETH/rETH. unstake: burn LST for ETH. ' +\n      'wrap: stETH → wstETH. unwrap: wstETH → stETH. ' +\n      'positions: view staking positions and APYs.',\n  }),\n  protocol: Type.Optional(Type.String({\n    description: 'Protocol: \"lido\" (stETH/wstETH), \"rocket_pool\" (rETH), \"coinbase\" (cbETH). Required for stake/unstake/wrap/unwrap.',\n  })),\n  amount: Type.Optional(Type.String({\n    description: 'Amount in ETH (for stake) or LST units (for unstake/wrap/unwrap). E.g. \"1.5\" for 1.5 ETH.',\n  })),\n  chain: Type.Optional(Type.String({\n    description: 'Chain for positions: \"ethereum\" (default) or \"base\". Staking always targets Ethereum mainnet.',\n  })),\n  address: Type.Optional(Type.String({\n    description: 'Wallet address to check positions for. Defaults to connected wallet.',\n  })),\n});\n\nexport function createDefiStakeTool() {\n  return {\n    name: 'defi_stake',\n    label: 'DeFi Staking',\n    ownerOnly: true,\n    description:\n      'Liquid staking operations. Stake ETH via Lido (stETH) or Rocket Pool (rETH). ' +\n      'Wrap stETH to wstETH for DeFi composability. View staking positions with live APYs. ' +\n      'Staking operations target Ethereum mainnet.',\n    parameters: DefiStakeSchema,\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      switch (action) {\n        case 'stake':\n          return handleStake(params);\n        case 'unstake':\n          return handleUnstake(params);\n        case 'wrap':\n          return handleWrap(params);\n        case 'unwrap':\n          return handleUnwrap(params);\n        case 'positions':\n          return handlePositions(params);\n        default:\n          return errorResult(`Unknown action: ${action}. Use: stake, unstake, wrap, unwrap, positions`);\n      }\n    },\n  };\n}\n\n// ── Helpers ──────────────────────────────────────────────────────────────────\n\nfunction requireConnectedWallet() {\n  const state = getWalletState();\n  if (!state.connected || !state.address) {\n    throw new Error('No wallet connected. Connect a wallet first.');\n  }\n  return state.address as `0x${string}`;\n}\n\nfunction resolveChainId(chain?: string): number {\n  if (!chain) return 1; // Default to Ethereum for staking\n  switch (chain.toLowerCase()) {\n    case 'base': return 8453;\n    case 'ethereum': case 'eth': case 'mainnet': default: return 1;\n  }\n}\n\n/**\n * Validate and parse an ETH amount string.\n * Rejects empty, non-numeric, negative, and zero amounts with clear errors.\n */\nfunction validateAndParseEther(amountInput: string): bigint {\n  const trimmed = amountInput.trim();\n  if (!trimmed) {\n    throw new Error('Amount cannot be empty.');\n  }\n  if (!/^\\d+(\\.\\d+)?$/.test(trimmed)) {\n    throw new Error(`Invalid amount \"${trimmed}\". Must be a positive number (e.g. \"1.5\", \"0.01\").`);\n  }\n  const parsed = parseFloat(trimmed);\n  if (parsed === 0) {\n    throw new Error('Amount must be greater than zero.');\n  }\n  return parseEther(trimmed);\n}\n\n// ── Action Handlers ─────────────────────────────────────────────────────────\n\nasync function handleStake(params: Record<string, unknown>) {\n  const protocolInput = readStringParam(params, 'protocol', { required: true });\n  const amountInput = readStringParam(params, 'amount', { required: true });\n  if (!protocolInput || !amountInput) {\n    return errorResult('Both protocol and amount are required for stake.');\n  }\n\n  try {\n    const userAddress = requireConnectedWallet();\n    const service = getStakingService();\n    const protocol = service.resolveProtocol(protocolInput);\n    if (!protocol) {\n      return errorResult(\n        `Unknown protocol: \"${protocolInput}\". Supported: lido (stETH), rocket_pool (rETH), coinbase (cbETH).`,\n      );\n    }\n\n    const amount = validateAndParseEther(amountInput);\n    const wallet = requireWalletClient();\n    const publicClient = requirePublicClient();\n\n    const result = await service.stakeEth(protocol, amount, userAddress, wallet, publicClient);\n\n    return jsonResult({\n      status: 'success',\n      action: 'stake',\n      protocol: result.protocol,\n      received: result.asset,\n      ethStaked: result.amount,\n      txHash: result.hash,\n      note: protocol === 'lido'\n        ? 'Received stETH. Use action=wrap to convert to wstETH for DeFi use.'\n        : undefined,\n    });\n  } catch (err) {\n    return errorResult(`Stake failed: ${err instanceof Error ? err.message : String(err)}`);\n  }\n}\n\nasync function handleUnstake(params: Record<string, unknown>) {\n  const protocolInput = readStringParam(params, 'protocol', { required: true });\n  const amountInput = readStringParam(params, 'amount', { required: true });\n  if (!protocolInput || !amountInput) {\n    return errorResult('Both protocol and amount are required for unstake.');\n  }\n\n  try {\n    const userAddress = requireConnectedWallet();\n    const service = getStakingService();\n    const protocol = service.resolveProtocol(protocolInput);\n    if (!protocol) {\n      return errorResult(\n        `Unknown protocol: \"${protocolInput}\". Supported: lido, rocket_pool, coinbase.`,\n      );\n    }\n\n    const amount = validateAndParseEther(amountInput);\n    const wallet = requireWalletClient();\n    const publicClient = requirePublicClient();\n\n    const result = await service.unstake(protocol, amount, userAddress, wallet, publicClient);\n\n    return jsonResult({\n      status: 'success',\n      action: 'unstake',\n      protocol: result.protocol,\n      burned: result.asset,\n      amount: result.amount,\n      txHash: result.hash,\n    });\n  } catch (err) {\n    return errorResult(`Unstake failed: ${err instanceof Error ? err.message : String(err)}`);\n  }\n}\n\nasync function handleWrap(params: Record<string, unknown>) {\n  const amountInput = readStringParam(params, 'amount', { required: true });\n  if (!amountInput) {\n    return errorResult('Amount of stETH to wrap is required.');\n  }\n\n  try {\n    const userAddress = requireConnectedWallet();\n    const service = getStakingService();\n    const wallet = requireWalletClient();\n    const publicClient = requirePublicClient();\n\n    const amount = validateAndParseEther(amountInput);\n    const result = await service.wrap(amount, userAddress, wallet, publicClient);\n\n    return jsonResult({\n      status: 'success',\n      action: 'wrap',\n      protocol: 'lido',\n      from: 'stETH',\n      to: 'wstETH',\n      stEthAmount: result.amount,\n      txHash: result.hash,\n      note: 'wstETH is the composable form — accepted by Aave, DeFi vaults, and L2 bridges.',\n    });\n  } catch (err) {\n    return errorResult(`Wrap failed: ${err instanceof Error ? err.message : String(err)}`);\n  }\n}\n\nasync function handleUnwrap(params: Record<string, unknown>) {\n  const amountInput = readStringParam(params, 'amount', { required: true });\n  if (!amountInput) {\n    return errorResult('Amount of wstETH to unwrap is required.');\n  }\n\n  try {\n    const userAddress = requireConnectedWallet();\n    const service = getStakingService();\n    const wallet = requireWalletClient();\n\n    const amount = validateAndParseEther(amountInput);\n    const publicClient = requirePublicClient();\n    const result = await service.unwrap(amount, userAddress, wallet, publicClient);\n\n    return jsonResult({\n      status: 'success',\n      action: 'unwrap',\n      protocol: 'lido',\n      from: 'wstETH',\n      to: 'stETH',\n      wstEthAmount: result.amount,\n      txHash: result.hash,\n    });\n  } catch (err) {\n    return errorResult(`Unwrap failed: ${err instanceof Error ? err.message : String(err)}`);\n  }\n}\n\nasync function handlePositions(params: Record<string, unknown>) {\n  try {\n    const state = getWalletState();\n    const addressInput = readStringParam(params, 'address') ?? state.address;\n    if (!addressInput) {\n      return errorResult('No wallet connected and no address provided.');\n    }\n\n    const chainId = resolveChainId(readStringParam(params, 'chain'));\n    const service = getStakingService();\n\n    // Get appropriate client for the chain\n    const rpcManager = getRpcManager();\n    const publicClient = await rpcManager.getClient(chainId);\n\n    const positions = await service.getPositions(\n      addressInput as `0x${string}`,\n      chainId,\n      publicClient,\n    );\n\n    // Fetch APY data\n    const apys = await service.getApys();\n\n    if (positions.length === 0) {\n      return jsonResult({\n        chain: chainId === 1 ? 'ethereum' : 'base',\n        address: addressInput,\n        positions: [],\n        message: 'No staking positions found on this chain.',\n        tip: chainId === 8453\n          ? 'LSTs on Base are bridged tokens. Staking is done on Ethereum mainnet.'\n          : 'Use action=stake protocol=lido amount=1.0 to stake ETH.',\n      });\n    }\n\n    const totalEth = positions.reduce(\n      (sum, p) => sum + parseFloat(p.balanceEth || p.balance),\n      0,\n    );\n\n    return jsonResult({\n      chain: chainId === 1 ? 'ethereum' : 'base',\n      address: addressInput,\n      totalEthEquivalent: totalEth.toFixed(6),\n      positions: positions.map(p => ({\n        protocol: p.protocol,\n        asset: p.asset,\n        balance: p.balance,\n        ethEquivalent: p.balanceEth,\n        apy: p.apy ?? 'unavailable',\n        chain: p.chain,\n      })),\n      apyRates: apys.length > 0\n        ? apys.map(a => ({\n            protocol: a.protocol,\n            symbol: a.symbol,\n            apy: `${a.apy.toFixed(2)}%`,\n            tvl: a.tvl > 1_000_000_000\n              ? `$${(a.tvl / 1_000_000_000).toFixed(1)}B`\n              : `$${(a.tvl / 1_000_000).toFixed(0)}M`,\n          }))\n        : undefined,\n    });\n  } catch (err) {\n    return errorResult(`Positions check failed: ${err instanceof Error ? err.message : String(err)}`);\n  }\n}\n"],"mappings":"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"}