{"version":3,"file":"nft-service.mjs","names":[],"sources":["../../../src/services/nft-service.ts"],"sourcesContent":["/**\n * NFT Service — Reservoir API client for NFT operations.\n *\n * Supports:\n *   - Collection floor prices and metadata\n *   - Token viewing and metadata resolution\n *   - Portfolio listing (user's NFTs)\n *   - Buy/list/offer execution via Reservoir order flow\n *   - Transfer via standard ERC-721 safeTransferFrom\n *\n * Uses Reservoir API (covers OpenSea, Blur, LooksRare marketplaces).\n * Requires RESERVOIR_API_KEY env var (free tier: 4 req/sec).\n */\n\nimport { getCredentialVault } from './credential-vault.js';\nimport { guardedFetch } from './endpoint-allowlist.js';\n\n// ── Types ───────────────────────────────────────────────────────────────────\n\nexport interface NftToken {\n  contract: string;\n  tokenId: string;\n  name: string | null;\n  description: string | null;\n  image: string | null;\n  collection: string | null;\n  owner: string | null;\n  lastSalePrice: string | null;\n  rarity: number | null;\n  attributes: Array<{ key: string; value: string }>;\n  chain: string;\n}\n\nexport interface NftCollection {\n  id: string;\n  name: string;\n  image: string | null;\n  floorPrice: string | null;\n  floorPriceCurrency: string;\n  volume24h: string | null;\n  totalSupply: number | null;\n  ownerCount: number | null;\n  chain: string;\n}\n\nexport interface NftPortfolioItem {\n  contract: string;\n  tokenId: string;\n  name: string | null;\n  image: string | null;\n  collection: string | null;\n  floorPrice: string | null;\n  lastSalePrice: string | null;\n  acquiredAt: string | null;\n}\n\nexport interface NftBuyResult {\n  status: string;\n  orderId: string | null;\n  txData: { to: string; data: string; value: string } | null;\n  price: string | null;\n  marketplace: string | null;\n}\n\nexport interface NftListResult {\n  status: string;\n  orderId: string | null;\n  steps: any[];\n}\n\n// ── Chain Configuration ─────────────────────────────────────────────────────\n\nconst CHAIN_API_BASE: Record<number, string> = {\n  1: 'https://api.reservoir.tools',\n  8453: 'https://api-base.reservoir.tools',\n  42161: 'https://api-arbitrum.reservoir.tools',\n  10: 'https://api-optimism.reservoir.tools',\n  137: 'https://api-polygon.reservoir.tools',\n};\n\nconst CHAIN_NAMES: Record<number, string> = {\n  1: 'ethereum', 8453: 'base', 42161: 'arbitrum', 10: 'optimism', 137: 'polygon',\n};\n\n// ── ERC-721 Minimal ABI ─────────────────────────────────────────────────────\n\nexport const ERC721_TRANSFER_ABI = [\n  {\n    name: 'safeTransferFrom',\n    inputs: [\n      { name: 'from', type: 'address' },\n      { name: 'to', type: 'address' },\n      { name: 'tokenId', type: 'uint256' },\n    ],\n    outputs: [],\n    stateMutability: 'nonpayable',\n    type: 'function',\n  },\n  {\n    name: 'ownerOf',\n    inputs: [{ name: 'tokenId', type: 'uint256' }],\n    outputs: [{ name: '', type: 'address' }],\n    stateMutability: 'view',\n    type: 'function',\n  },\n] as const;\n\n// ── Service ─────────────────────────────────────────────────────────────────\n\nexport class NftService {\n  private metadataCache = new Map<string, { data: NftToken; ts: number }>();\n  private readonly CACHE_TTL = 300_000; // 5 minutes\n\n  getApiKey(): string | null {\n    return getCredentialVault().getSecret('nft.reservoir.apiKey', 'nft');\n  }\n\n  private getApiBase(chainId: number): string {\n    return CHAIN_API_BASE[chainId] ?? CHAIN_API_BASE[8453]!;\n  }\n\n  private async reservoirFetch(\n    chainId: number,\n    path: string,\n    params?: Record<string, string>,\n    method = 'GET',\n    body?: unknown,\n  ): Promise<any> {\n    const apiKey = this.getApiKey();\n    if (!apiKey) {\n      throw new Error(\n        'RESERVOIR_API_KEY not configured. Get a free key at https://reservoir.tools ' +\n        'then set: /flykeys set RESERVOIR_API_KEY your_key',\n      );\n    }\n\n    const base = this.getApiBase(chainId);\n    const url = new URL(path, base);\n    if (params) {\n      for (const [k, v] of Object.entries(params)) {\n        url.searchParams.set(k, v);\n      }\n    }\n\n    const headers: Record<string, string> = {\n      Accept: 'application/json',\n      'x-api-key': apiKey,\n    };\n    if (body) headers['Content-Type'] = 'application/json';\n\n    const response = await guardedFetch(url.toString(), {\n      method,\n      headers,\n      body: body ? JSON.stringify(body) : undefined,\n      signal: AbortSignal.timeout(15_000),\n    });\n\n    if (!response.ok) {\n      const text = await response.text().catch(() => '');\n      throw new Error(`Reservoir API ${response.status}: ${text || response.statusText}`);\n    }\n\n    return response.json();\n  }\n\n  // ── Token View ─────────────────────────────────────────────────────\n\n  async getToken(contract: string, tokenId: string, chainId = 8453): Promise<NftToken> {\n    const cacheKey = `${chainId}:${contract}:${tokenId}`;\n    const cached = this.metadataCache.get(cacheKey);\n    if (cached && Date.now() - cached.ts < this.CACHE_TTL) return cached.data;\n\n    const data = await this.reservoirFetch(chainId, '/tokens/v7', {\n      tokens: `${contract}:${tokenId}`,\n      includeAttributes: 'true',\n      includeLastSale: 'true',\n    });\n\n    const token = data.tokens?.[0]?.token;\n    if (!token) throw new Error(`Token not found: ${contract}:${tokenId}`);\n\n    const result: NftToken = {\n      contract: token.contract,\n      tokenId: token.tokenId,\n      name: token.name ?? null,\n      description: token.description ?? null,\n      image: token.image ?? token.imageSmall ?? null,\n      collection: token.collection?.name ?? null,\n      owner: token.owner ?? data.tokens?.[0]?.ownership?.owner ?? null,\n      lastSalePrice: data.tokens?.[0]?.market?.lastSale?.price?.amount?.native\n        ? `${data.tokens[0].market.lastSale.price.amount.native} ETH`\n        : null,\n      rarity: token.rarityRank ?? null,\n      attributes: (token.attributes ?? []).map((a: any) => ({\n        key: a.key,\n        value: a.value,\n      })),\n      chain: CHAIN_NAMES[chainId] ?? String(chainId),\n    };\n\n    this.metadataCache.set(cacheKey, { data: result, ts: Date.now() });\n    return result;\n  }\n\n  // ── Collection Floor ───────────────────────────────────────────────\n\n  async getCollectionFloor(collectionIdOrSlug: string, chainId = 8453): Promise<NftCollection> {\n    // Try as collection ID (contract address) first, then as slug\n    const params: Record<string, string> = collectionIdOrSlug.startsWith('0x')\n      ? { id: collectionIdOrSlug }\n      : { slug: collectionIdOrSlug };\n\n    const data = await this.reservoirFetch(chainId, '/collections/v7', {\n      ...params,\n      includeOwnerCount: 'true',\n    });\n\n    const collection = data.collections?.[0];\n    if (!collection) throw new Error(`Collection not found: ${collectionIdOrSlug}`);\n\n    return {\n      id: collection.id,\n      name: collection.name ?? 'Unknown',\n      image: collection.image ?? null,\n      floorPrice: collection.floorAsk?.price?.amount?.native\n        ? `${collection.floorAsk.price.amount.native}`\n        : null,\n      floorPriceCurrency: collection.floorAsk?.price?.currency?.symbol ?? 'ETH',\n      volume24h: collection.volume?.['1day'] !== undefined\n        ? `${collection.volume['1day']}`\n        : null,\n      totalSupply: collection.tokenCount ? parseInt(collection.tokenCount) : null,\n      ownerCount: collection.ownerCount ? parseInt(collection.ownerCount) : null,\n      chain: CHAIN_NAMES[chainId] ?? String(chainId),\n    };\n  }\n\n  // ── Portfolio ──────────────────────────────────────────────────────\n\n  async getPortfolio(\n    ownerAddress: string,\n    chainId = 8453,\n    limit = 50,\n  ): Promise<NftPortfolioItem[]> {\n    const data = await this.reservoirFetch(chainId, '/users/tokens/v10', {\n      users: ownerAddress,\n      limit: String(Math.min(limit, 200)),\n      includeLastSale: 'true',\n      sortBy: 'acquiredAt',\n      sortDirection: 'desc',\n    });\n\n    const tokens: any[] = data.tokens ?? [];\n\n    return tokens.map((item: any) => ({\n      contract: item.token?.contract ?? '',\n      tokenId: item.token?.tokenId ?? '',\n      name: item.token?.name ?? null,\n      image: item.token?.image ?? item.token?.imageSmall ?? null,\n      collection: item.token?.collection?.name ?? null,\n      floorPrice: item.token?.collection?.floorAskPrice?.amount?.native\n        ? `${item.token.collection.floorAskPrice.amount.native} ETH`\n        : null,\n      lastSalePrice: item.market?.lastSale?.price?.amount?.native\n        ? `${item.market.lastSale.price.amount.native} ETH`\n        : null,\n      acquiredAt: item.ownership?.acquiredAt ?? null,\n    }));\n  }\n\n  // ── Buy ────────────────────────────────────────────────────────────\n\n  async getBuyOrder(\n    contract: string,\n    tokenId: string,\n    takerAddress: string,\n    chainId = 8453,\n  ): Promise<NftBuyResult> {\n    const data = await this.reservoirFetch(\n      chainId,\n      '/execute/buy/v7',\n      {},\n      'POST',\n      {\n        items: [{ token: `${contract}:${tokenId}`, quantity: 1 }],\n        taker: takerAddress,\n        skipBalanceCheck: false,\n      },\n    );\n\n    const step = data.steps?.[0];\n    const item = step?.items?.[0];\n\n    return {\n      status: item?.status ?? data.message ?? 'unknown',\n      orderId: item?.orderId ?? null,\n      txData: item?.data\n        ? { to: item.data.to, data: item.data.data, value: item.data.value ?? '0' }\n        : null,\n      price: data.path?.[0]?.buyInQuote\n        ? `${data.path[0].buyInQuote} ${data.path[0].buyInCurrency?.symbol ?? 'ETH'}`\n        : null,\n      marketplace: data.path?.[0]?.source ?? null,\n    };\n  }\n\n  // ── List (Sell) ────────────────────────────────────────────────────\n\n  async getListOrder(\n    contract: string,\n    tokenId: string,\n    makerAddress: string,\n    priceWei: string,\n    chainId = 8453,\n    expirationDays = 30,\n  ): Promise<NftListResult> {\n    const expiration = Math.floor(Date.now() / 1000) + expirationDays * 86400;\n\n    const data = await this.reservoirFetch(\n      chainId,\n      '/execute/list/v5',\n      {},\n      'POST',\n      {\n        maker: makerAddress,\n        token: `${contract}:${tokenId}`,\n        weiPrice: priceWei,\n        expirationTime: String(expiration),\n        orderbook: 'reservoir',\n      },\n    );\n\n    return {\n      status: data.steps?.[0]?.items?.[0]?.status ?? 'pending',\n      orderId: data.steps?.[0]?.items?.[0]?.orderId ?? null,\n      steps: data.steps ?? [],\n    };\n  }\n}\n\n// ── Singleton ───────────────────────────────────────────────────────────────\n\nlet _instance: NftService | null = null;\n\nexport function getNftService(): NftService {\n  if (!_instance) {\n    _instance = new NftService();\n  }\n  return _instance;\n}\n\nexport function resetNftService(): void {\n  _instance = null;\n}\n"],"mappings":"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"}