import axios from "axios"; import { authenticate } from "../middleware/auth.js"; import Item from "../models/Item.js"; import Trade from "../models/Trade.js"; import Transaction from "../models/Transaction.js"; import SiteConfig from "../models/SiteConfig.js"; import { config } from "../config/index.js"; import pricingService from "../services/pricing.js"; import marketPriceService from "../services/marketPrice.js"; import { getSteamBotManager } from "../services/steamBot.js"; /** * Inventory routes for fetching and listing Steam items * @param {FastifyInstance} fastify * @param {Object} options */ export default async function inventoryRoutes(fastify, options) { // GET /inventory/steam - Fetch user's Steam inventory fastify.get( "/steam", { preHandler: authenticate, schema: { querystring: { type: "object", properties: { game: { type: "string", enum: ["cs2", "rust"], default: "cs2" }, }, }, }, }, async (request, reply) => { try { const { game = "cs2" } = request.query; const steamId = request.user.steamId; if (!steamId) { return reply.status(400).send({ success: false, message: "Steam ID not found", }); } // Map game to Steam app ID const appIds = { cs2: 730, // Counter-Strike 2 rust: 252490, // Rust }; const appId = appIds[game]; const contextId = 2; // Standard Steam inventory context console.log( `๐ŸŽฎ Fetching ${game.toUpperCase()} inventory for Steam ID: ${steamId}` ); // Get Steam API key from environment (check both possible env var names) const steamApiKey = process.env.STEAM_APIS_KEY || process.env.STEAM_API_KEY || config.steam?.apiKey; if (!steamApiKey) { console.error("โŒ STEAM_API_KEY or STEAM_APIS_KEY not configured"); return reply.status(500).send({ success: false, message: "Steam API is not configured. Please contact support.", }); } // Fetch from SteamAPIs.com const steamApiUrl = `https://api.steamapis.com/steam/inventory/${steamId}/${appId}/${contextId}`; console.log(`๐Ÿ“ก Calling: ${steamApiUrl}`); const response = await axios.get(steamApiUrl, { params: { api_key: steamApiKey, }, timeout: 15000, headers: { "User-Agent": "TurboTrades/1.0", }, }); if (!response.data || !response.data.assets) { console.log("โš ๏ธ Empty inventory or private profile"); return reply.send({ success: true, items: [], message: "Inventory is empty or private", }); } const { assets, descriptions } = response.data; // Create a map of descriptions for quick lookup const descMap = new Map(); descriptions.forEach((desc) => { const key = `${desc.classid}_${desc.instanceid}`; descMap.set(key, desc); }); // Process items const items = assets .map((asset) => { const key = `${asset.classid}_${asset.instanceid}`; const desc = descMap.get(key); if (!desc) return null; // Parse item details const item = { assetid: asset.assetid, appid: appId, contextid: contextId, classid: asset.classid, instanceid: asset.instanceid, name: desc.market_hash_name || desc.name || "Unknown Item", type: desc.type || "", image: desc.icon_url ? `https://community.cloudflare.steamstatic.com/economy/image/${desc.icon_url}` : null, nameColor: desc.name_color || null, backgroundColor: desc.background_color || null, marketable: desc.marketable === 1, tradable: desc.tradable === 1, commodity: desc.commodity === 1, tags: desc.tags || [], descriptions: desc.descriptions || [], }; // Extract rarity from tags const rarityTag = item.tags.find( (tag) => tag.category === "Rarity" ); if (rarityTag) { item.rarity = rarityTag.internal_name || rarityTag.localized_tag_name; } // Extract exterior (wear) from tags for CS2 if (game === "cs2") { const exteriorTag = item.tags.find( (tag) => tag.category === "Exterior" ); if (exteriorTag) { const wearMap = { "Factory New": "fn", "Minimal Wear": "mw", "Field-Tested": "ft", "Well-Worn": "ww", "Battle-Scarred": "bs", }; item.wear = wearMap[exteriorTag.localized_tag_name] || null; item.wearName = exteriorTag.localized_tag_name; } } // Extract category const categoryTag = item.tags.find( (tag) => tag.category === "Type" || tag.category === "Weapon" ); if (categoryTag) { item.category = categoryTag.internal_name || categoryTag.localized_tag_name; } // Check if StatTrak or Souvenir item.statTrak = item.name.includes("StatTrakโ„ข"); item.souvenir = item.name.includes("Souvenir"); // Detect phase for Doppler items const descriptionText = item.descriptions .map((d) => d.value || "") .join(" "); item.phase = pricingService.detectPhase(item.name, descriptionText); return item; }) .filter((item) => item !== null && item.marketable && item.tradable); console.log(`โœ… Found ${items.length} marketable items in inventory`); // Enrich items with market prices (fast database lookup) console.log(`๐Ÿ’ฐ Adding market prices...`); // Get site config for payout rate const siteConfig = await SiteConfig.getConfig(); let payoutRate = siteConfig.instantSell.payoutRate || 0.6; // Check for game-specific payout rate if (game === "cs2" && siteConfig.instantSell.cs2?.payoutRate) { payoutRate = siteConfig.instantSell.cs2.payoutRate; } else if (game === "rust" && siteConfig.instantSell.rust?.payoutRate) { payoutRate = siteConfig.instantSell.rust.payoutRate; } console.log( `๐Ÿ’ต Instant sell payout rate: ${(payoutRate * 100).toFixed( 0 )}% of market price` ); // Get all item names for batch lookup const itemNames = items.map((item) => item.name); console.log(`๐Ÿ“‹ Looking up prices for ${itemNames.length} items`); console.log(`๐ŸŽฎ Game: ${game}`); console.log(`๐Ÿ“ First 3 item names:`, itemNames.slice(0, 3)); const priceMap = await marketPriceService.getPrices(itemNames, game); const foundPrices = Object.keys(priceMap).length; console.log( `๐Ÿ’ฐ Found prices for ${foundPrices}/${itemNames.length} items` ); // Add prices to items (applying payout rate for instant sell) const enrichedItems = items.map((item) => { const marketPrice = priceMap[item.name] || null; return { ...item, marketPrice: marketPrice ? parseFloat((marketPrice * payoutRate).toFixed(2)) : null, fullMarketPrice: marketPrice, // Keep original for reference payoutRate: payoutRate, hasPriceData: !!marketPrice, }; }); // Log items without prices const itemsWithoutPrices = enrichedItems.filter( (item) => !item.marketPrice ); if (itemsWithoutPrices.length > 0) { console.log(`โš ๏ธ ${itemsWithoutPrices.length} items without prices:`); itemsWithoutPrices.slice(0, 5).forEach((item) => { console.log(` - ${item.name}`); }); } console.log(`โœ… Prices added to ${enrichedItems.length} items`); return reply.send({ success: true, items: enrichedItems, total: enrichedItems.length, }); } catch (error) { console.error("โŒ Error fetching Steam inventory:", error.message); console.error("Error details:", error.response?.data || error.message); if (error.response?.status === 401) { return reply.status(500).send({ success: false, message: "Steam API authentication failed. Please contact support.", }); } if (error.response?.status === 403) { return reply.status(403).send({ success: false, message: "Steam inventory is private. Please make your inventory public in Steam settings.", }); } if (error.response?.status === 404) { return reply.status(404).send({ success: false, message: "Steam profile not found or inventory is empty.", }); } if (error.response?.status === 429) { return reply.status(429).send({ success: false, message: "Steam API rate limit exceeded. Please try again in a few moments.", }); } if (error.code === "ECONNABORTED" || error.code === "ETIMEDOUT") { return reply.status(504).send({ success: false, message: "Steam API request timed out. Please try again.", }); } return reply.status(500).send({ success: false, message: "Failed to fetch Steam inventory. Please try again later.", }); } } ); // POST /inventory/price - Get pricing for items fastify.post( "/price", { preHandler: authenticate, schema: { body: { type: "object", required: ["items"], properties: { items: { type: "array", items: { type: "object", required: ["name"], properties: { name: { type: "string" }, assetid: { type: "string" }, wear: { type: "string" }, }, }, }, }, }, }, }, async (request, reply) => { try { const { items } = request.body; // Use fast market price database for instant lookups const pricedItems = await Promise.all( items.map(async (item) => { try { // Use market price service for instant lookup (<1ms) const marketPrice = await marketPriceService.getPrice( item.name, "cs2" // TODO: Get from request or item ); return { ...item, estimatedPrice: marketPrice, currency: "USD", hasPriceData: marketPrice !== null, }; } catch (err) { console.error(`Error pricing item ${item.name}:`, err.message); return { ...item, estimatedPrice: null, currency: "USD", hasPriceData: false, error: "Price data not available", }; } }) ); // Filter out items without price data const itemsWithPrices = pricedItems.filter( (item) => item.estimatedPrice !== null ); return reply.send({ success: true, items: itemsWithPrices, total: items.length, priced: itemsWithPrices.length, noPriceData: items.length - itemsWithPrices.length, }); } catch (error) { console.error("Error pricing items:", error); return reply.status(500).send({ success: false, message: "Failed to calculate item prices", }); } } ); // POST /inventory/sell - Create Steam trade offer to sell items fastify.post( "/sell", { preHandler: authenticate, schema: { body: { type: "object", required: ["items", "tradeUrl"], properties: { items: { type: "array", items: { type: "object", required: [ "assetid", "appid", "contextid", "name", "price", "image", ], properties: { assetid: { type: "string" }, appid: { type: "number" }, contextid: { type: "string" }, name: { type: "string" }, price: { type: "number" }, image: { type: "string" }, wear: { type: "string" }, rarity: { type: "string" }, category: { type: "string" }, statTrak: { type: "boolean" }, souvenir: { type: "boolean" }, }, }, }, tradeUrl: { type: "string" }, }, }, }, }, async (request, reply) => { // Declare variables outside try block for catch block access let userId, steamId, items, tradeUrl, botManager, bypassBots; try { items = request.body.items; tradeUrl = request.body.tradeUrl; userId = request.user._id; steamId = request.user.steamId; console.log("๐Ÿ” Sell endpoint called:", { userId, steamId, itemCount: items?.length, hasTradeUrl: !!tradeUrl, nodeEnv: process.env.NODE_ENV, bypassBot: process.env.BYPASS_BOT_REQUIREMENT, }); if (!items || items.length === 0) { return reply.status(400).send({ success: false, message: "No items selected", }); } if (!tradeUrl) { return reply.status(400).send({ success: false, message: "Trade URL is required. Please set your trade URL in your profile.", }); } // Calculate total value const totalValue = items.reduce((sum, item) => sum + item.price, 0); // Get bot manager botManager = getSteamBotManager(); // In development mode, allow bypassing bot requirement const isDevelopmentMode = process.env.NODE_ENV === "development"; bypassBots = process.env.BYPASS_BOT_REQUIREMENT === "true"; if (!botManager.isInitialized && !bypassBots) { return reply.status(503).send({ success: false, message: "Trade system is currently unavailable. Please try again later.", }); } // Development mode: Mock trade creation if (bypassBots || !botManager.isInitialized) { console.log( "โš ๏ธ DEVELOPMENT MODE: Creating mock trade (no real Steam bot)" ); // Generate mock verification code const mockCode = Math.random() .toString(36) .substring(2, 8) .toUpperCase(); const mockOfferId = `DEV_${Date.now()}`; const mockTradeOfferUrl = `https://steamcommunity.com/tradeoffer/${mockOfferId}`; // Create trade record in database const trade = new Trade({ offerId: mockOfferId, botId: "dev-bot", userId: userId, steamId: steamId, tradeUrl: tradeUrl, tradeOfferUrl: mockTradeOfferUrl, items: items.map((item) => ({ assetId: item.assetid, name: item.name, price: item.price, image: item.image, game: item.appid === 730 ? "cs2" : "rust", wear: item.wear, rarity: item.rarity, category: item.category, statTrak: item.statTrak, souvenir: item.souvenir, })), totalValue, userReceives: totalValue, verificationCode: mockCode, state: "pending", }); await trade.save(); console.log( `โœ… Mock trade created: ${mockOfferId} with code: ${mockCode}` ); // Send WebSocket notification if (fastify.websocketManager) { fastify.websocketManager.sendToUser(steamId, { type: "trade_created", data: { tradeId: trade._id, offerId: mockOfferId, verificationCode: mockCode, tradeOfferUrl: mockTradeOfferUrl, itemCount: items.length, totalValue, botId: "dev-bot", status: "pending", timestamp: Date.now(), isDevelopment: true, }, }); } return reply.send({ success: true, message: "Mock trade created (Development Mode)", trade: { tradeId: trade._id, offerId: mockOfferId, verificationCode: mockCode, tradeOfferUrl: mockTradeOfferUrl, itemCount: items.length, totalValue, status: "pending", botId: "dev-bot", isDevelopment: true, }, }); } // Prepare items for Steam trade (format required by steam-tradeoffer-manager) const itemsToReceive = items.map((item) => ({ assetid: item.assetid, appid: item.appid, contextid: item.contextid, })); console.log( `๐Ÿ“ค Creating trade offer for user ${request.user.username} (${ items.length } items, $${totalValue.toFixed(2)})` ); // Create trade offer via bot let tradeResult; try { tradeResult = await botManager.createTradeOffer({ tradeUrl, itemsToReceive, userId: steamId, metadata: { username: request.user.username, itemCount: items.length, totalValue, }, }); } catch (botError) { console.error("Bot trade creation error:", botError); return reply.status(503).send({ success: false, message: botError.message || "Failed to create trade offer. Please try again.", }); } // Create trade record in database const trade = new Trade({ offerId: tradeResult.offerId, botId: tradeResult.botId, userId: userId, steamId: steamId, tradeUrl: tradeUrl, tradeOfferUrl: tradeResult.tradeOfferUrl, items: items.map((item) => ({ assetId: item.assetid, name: item.name, price: item.price, image: item.image, game: item.appid === 730 ? "cs2" : "rust", wear: item.wear, rarity: item.rarity, category: item.category, statTrak: item.statTrak, souvenir: item.souvenir, })), totalValue, userReceives: totalValue, verificationCode: tradeResult.code, state: "pending", }); await trade.save(); console.log( `โœ… Trade offer created: ${tradeResult.offerId} with verification code: ${tradeResult.code}` ); // Send immediate WebSocket notification with verification code if (fastify.websocketManager) { fastify.websocketManager.sendToUser(steamId, { type: "trade_created", data: { tradeId: trade._id, offerId: tradeResult.offerId, verificationCode: tradeResult.code, tradeOfferUrl: tradeResult.tradeOfferUrl, itemCount: items.length, totalValue, botId: tradeResult.botId, status: "pending", timestamp: Date.now(), }, }); } return reply.send({ success: true, message: "Trade offer created successfully", trade: { tradeId: trade._id, offerId: tradeResult.offerId, verificationCode: tradeResult.code, tradeOfferUrl: tradeResult.tradeOfferUrl, itemCount: items.length, totalValue, status: "pending", botId: tradeResult.botId, }, }); } catch (error) { console.error("โŒ Error creating sell trade:", error); console.error("Error stack:", error.stack); console.error("Error details:", { message: error.message, name: error.name, userId, steamId, itemCount: items?.length, tradeUrl: tradeUrl ? "present" : "missing", bypassBots, isInitialized: botManager?.isInitialized, }); return reply.status(500).send({ success: false, message: error.message || "Failed to create trade offer. Please try again.", }); } } ); // GET /inventory/trades - Get user's trades fastify.get( "/trades", { preHandler: authenticate, }, async (request, reply) => { try { const userId = request.user._id; const trades = await Trade.find({ user: userId }) .sort({ createdAt: -1 }) .limit(50); return reply.send({ success: true, trades, }); } catch (error) { console.error("Error fetching trades:", error); return reply.status(500).send({ success: false, message: "Failed to fetch trades", }); } } ); // GET /inventory/trade/:tradeId - Get specific trade details fastify.get( "/trade/:tradeId", { preHandler: authenticate, }, async (request, reply) => { try { const { tradeId } = request.params; const userId = request.user._id; const trade = await Trade.findOne({ _id: tradeId, userId: userId }); if (!trade) { return reply.status(404).send({ success: false, message: "Trade not found", }); } return reply.send({ success: true, trade, }); } catch (error) { console.error("Error fetching trade:", error); return reply.status(500).send({ success: false, message: "Failed to fetch trade details", }); } } ); // POST /inventory/trade/:tradeId/cancel - Cancel a pending trade fastify.post( "/trade/:tradeId/cancel", { preHandler: authenticate, }, async (request, reply) => { try { const { tradeId } = request.params; const userId = request.user._id; const steamId = request.user.steamId; const trade = await Trade.findOne({ _id: tradeId, userId: userId }); if (!trade) { return reply.status(404).send({ success: false, message: "Trade not found", }); } if (trade.state !== "pending") { return reply.status(400).send({ success: false, message: "Only pending trades can be cancelled", }); } // Cancel via bot manager const botManager = getSteamBotManager(); await botManager.cancelTradeOffer(trade.offerId, trade.botId); // Update trade status trade.state = "canceled"; await trade.save(); // Notify via WebSocket if (fastify.websocketManager) { fastify.websocketManager.sendToUser(steamId, { type: "trade_cancelled", data: { tradeId: trade._id, offerId: trade.offerId, timestamp: Date.now(), }, }); } return reply.send({ success: true, message: "Trade cancelled successfully", }); } catch (error) { console.error("Error cancelling trade:", error); return reply.status(500).send({ success: false, message: "Failed to cancel trade", }); } } ); // POST /inventory/trade/:tradeId/complete - Complete trade (development mode only) fastify.post( "/trade/:tradeId/complete", { preHandler: authenticate, }, async (request, reply) => { try { const { tradeId } = request.params; const userId = request.user._id; const steamId = request.user.steamId; // Only allow in development mode if ( process.env.NODE_ENV !== "development" && process.env.BYPASS_BOT_REQUIREMENT !== "true" ) { return reply.status(403).send({ success: false, message: "This endpoint is only available in development mode", }); } const trade = await Trade.findOne({ _id: tradeId, user: userId }); if (!trade) { return reply.status(404).send({ success: false, message: "Trade not found", }); } if (trade.state !== "pending") { return reply.status(400).send({ success: false, message: "Only pending trades can be completed", }); } // Simulate trade acceptance console.log( `๐Ÿงช DEV MODE: Manually completing trade ${tradeId} for user ${request.user.username}` ); // Credit user balance request.user.balance += trade.totalValue; await request.user.save(); // Update trade status trade.state = "accepted"; trade.completedAt = new Date(); await trade.save(); // Create transaction record const transaction = new Transaction({ user: userId, type: "sale", amount: trade.totalValue, description: `Sold ${trade.items.length} item(s) (DEV MODE)`, status: "completed", metadata: { tradeId: trade._id, offerId: trade.offerId, botId: trade.botId, itemCount: trade.items.length, verificationCode: trade.verificationCode, isDevelopment: true, }, }); await transaction.save(); console.log( `โœ… DEV MODE: Credited $${trade.totalValue.toFixed(2)} to user ${ request.user.username } (Balance: $${request.user.balance.toFixed(2)})` ); // Notify via WebSocket if (fastify.websocketManager) { fastify.websocketManager.sendToUser(steamId, { type: "trade_completed", data: { tradeId: trade._id, offerId: trade.offerId, amount: trade.totalValue, newBalance: request.user.balance, itemCount: trade.items.length, timestamp: Date.now(), isDevelopment: true, }, }); fastify.websocketManager.sendToUser(steamId, { type: "balance_update", data: { balance: request.user.balance, change: trade.totalValue, reason: "trade_completed", }, }); } return reply.send({ success: true, message: "Trade completed successfully (Development Mode)", trade: { tradeId: trade._id, status: "completed", amount: trade.totalValue, newBalance: request.user.balance, }, }); } catch (error) { console.error("Error completing trade:", error); return reply.status(500).send({ success: false, message: "Failed to complete trade", }); } } ); }