added steambot, trades and trasctions.
This commit is contained in:
500
TRADE_LINKS.md
Normal file
500
TRADE_LINKS.md
Normal file
@@ -0,0 +1,500 @@
|
|||||||
|
# Trade Links - Feature Documentation
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Trade offer links are now automatically generated and displayed throughout the trade flow, allowing users to quickly access their Steam trade offers with a single click.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔗 Trade Link Format
|
||||||
|
|
||||||
|
Steam trade offer links follow this format:
|
||||||
|
```
|
||||||
|
https://steamcommunity.com/tradeoffer/{OFFER_ID}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```
|
||||||
|
https://steamcommunity.com/tradeoffer/7182364598
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ Where Trade Links Appear
|
||||||
|
|
||||||
|
### 1. **Sell Page Modal** (Trade Created State)
|
||||||
|
|
||||||
|
When a trade is successfully created, the modal displays:
|
||||||
|
|
||||||
|
- ✅ Large verification code
|
||||||
|
- 🔗 **"Open Trade in Steam" button** (primary blue button)
|
||||||
|
- 📋 Trade details
|
||||||
|
- 📝 Instructions
|
||||||
|
|
||||||
|
**Button Style:**
|
||||||
|
- Gradient background (primary → primary-dark)
|
||||||
|
- External link icon
|
||||||
|
- Opens in new tab
|
||||||
|
- Full width
|
||||||
|
- Prominent placement above trade details
|
||||||
|
|
||||||
|
**Location:** Between verification code and trade details
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. **Transactions Page - Pending Trades List**
|
||||||
|
|
||||||
|
Each pending trade shows:
|
||||||
|
|
||||||
|
- 🟡 Yellow highlight with pulsing indicator
|
||||||
|
- 💰 Total value
|
||||||
|
- 🔢 Verification code (inline)
|
||||||
|
- 🔗 **"Open" button** (small, primary blue)
|
||||||
|
- ℹ️ "Details" button (transparent)
|
||||||
|
|
||||||
|
**Button Style:**
|
||||||
|
- Compact size
|
||||||
|
- External link icon
|
||||||
|
- Primary background
|
||||||
|
- Opens in new tab
|
||||||
|
|
||||||
|
**Location:** In the action buttons area (right side)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. **Transactions Page - Trade Details Modal**
|
||||||
|
|
||||||
|
When clicking "Details" on a pending trade:
|
||||||
|
|
||||||
|
- 📋 Full trade information
|
||||||
|
- 🔢 Large verification code display
|
||||||
|
- 🔗 **"Open Trade in Steam" button** (primary blue)
|
||||||
|
- 📝 Step-by-step instructions
|
||||||
|
|
||||||
|
**Button Style:**
|
||||||
|
- Same as Sell Page modal
|
||||||
|
- Full width
|
||||||
|
- Placed below verification code
|
||||||
|
|
||||||
|
**Location:** Above trade info section
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 User Experience Flow
|
||||||
|
|
||||||
|
### Complete Journey with Trade Links:
|
||||||
|
|
||||||
|
1. **User creates trade on Sell page**
|
||||||
|
- Modal opens with verification code
|
||||||
|
- User sees "Open Trade in Steam" button
|
||||||
|
|
||||||
|
2. **User clicks button**
|
||||||
|
- Opens Steam in new tab (desktop) or app (mobile)
|
||||||
|
- Automatically navigates to specific trade offer
|
||||||
|
|
||||||
|
3. **User verifies code**
|
||||||
|
- Compares code shown on site with Steam trade message
|
||||||
|
- Code matches = safe to accept
|
||||||
|
|
||||||
|
4. **Alternative: Check Later**
|
||||||
|
- User closes modal
|
||||||
|
- Goes to Transactions page
|
||||||
|
- Sees pending trade with "Open" button
|
||||||
|
- Clicks to open trade directly
|
||||||
|
- Or clicks "Details" for full modal view
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💻 Technical Implementation
|
||||||
|
|
||||||
|
### Backend (Bot Service)
|
||||||
|
|
||||||
|
**File:** `services/steamBot.js`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Generate trade offer URL from offer ID
|
||||||
|
const tradeOfferUrl = `https://steamcommunity.com/tradeoffer/${offer.id}`;
|
||||||
|
|
||||||
|
// Include in bot response
|
||||||
|
return {
|
||||||
|
offerId: offer.id,
|
||||||
|
verificationCode,
|
||||||
|
tradeOfferUrl, // ← New field
|
||||||
|
botId: this.botId,
|
||||||
|
// ...
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**WebSocket Notifications:**
|
||||||
|
All trade events now include `tradeOfferUrl`:
|
||||||
|
- `trade_sent`
|
||||||
|
- `trade_confirmed`
|
||||||
|
- `trade_created`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Backend (Inventory Routes)
|
||||||
|
|
||||||
|
**File:** `routes/inventory.js`
|
||||||
|
|
||||||
|
**Development Mode:**
|
||||||
|
```javascript
|
||||||
|
const mockOfferId = `DEV_${Date.now()}`;
|
||||||
|
const mockTradeOfferUrl = `https://steamcommunity.com/tradeoffer/${mockOfferId}`;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Trade Model:**
|
||||||
|
```javascript
|
||||||
|
const trade = new Trade({
|
||||||
|
offerId,
|
||||||
|
tradeOfferUrl, // ← Saved to database
|
||||||
|
verificationCode,
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**API Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"trade": {
|
||||||
|
"tradeId": "...",
|
||||||
|
"offerId": "...",
|
||||||
|
"verificationCode": "A3X9K2",
|
||||||
|
"tradeOfferUrl": "https://steamcommunity.com/tradeoffer/...",
|
||||||
|
"itemCount": 3,
|
||||||
|
"totalValue": 75.50
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Frontend (Sell Page)
|
||||||
|
|
||||||
|
**File:** `frontend/src/views/SellPage.vue`
|
||||||
|
|
||||||
|
**Button Component:**
|
||||||
|
```vue
|
||||||
|
<a
|
||||||
|
v-if="currentTrade?.tradeOfferUrl"
|
||||||
|
:href="currentTrade.tradeOfferUrl"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="w-full px-6 py-3 bg-gradient-to-r from-primary to-primary-dark text-surface-dark font-semibold rounded-lg hover:opacity-90 transition-opacity flex items-center justify-center gap-2 text-center"
|
||||||
|
>
|
||||||
|
<ExternalLink class="w-5 h-5" />
|
||||||
|
Open Trade in Steam
|
||||||
|
</a>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Data Flow:**
|
||||||
|
```javascript
|
||||||
|
// Trade created - store URL
|
||||||
|
currentTrade.value = {
|
||||||
|
tradeId: trade.tradeId,
|
||||||
|
offerId: trade.offerId,
|
||||||
|
verificationCode: trade.verificationCode,
|
||||||
|
tradeOfferUrl: trade.tradeOfferUrl, // ← From API response
|
||||||
|
// ...
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Frontend (Transactions Page)
|
||||||
|
|
||||||
|
**File:** `frontend/src/views/TransactionsPage.vue`
|
||||||
|
|
||||||
|
**Pending Trades List Button:**
|
||||||
|
```vue
|
||||||
|
<a
|
||||||
|
v-if="trade.tradeOfferUrl"
|
||||||
|
:href="trade.tradeOfferUrl"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="px-4 py-2 bg-primary hover:bg-primary-dark text-surface-dark font-medium rounded-lg transition-colors text-sm flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<ExternalLink class="w-4 h-4" />
|
||||||
|
Open
|
||||||
|
</a>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Trade Details Modal Button:**
|
||||||
|
```vue
|
||||||
|
<a
|
||||||
|
v-if="selectedTrade.tradeOfferUrl"
|
||||||
|
:href="selectedTrade.tradeOfferUrl"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="w-full px-6 py-3 bg-gradient-to-r from-primary to-primary-dark text-surface-dark font-semibold rounded-lg hover:opacity-90 transition-opacity flex items-center justify-center gap-2 text-center mb-4"
|
||||||
|
>
|
||||||
|
<ExternalLink class="w-5 h-5" />
|
||||||
|
Open Trade in Steam
|
||||||
|
</a>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📱 Platform Behavior
|
||||||
|
|
||||||
|
### Desktop (Web Browser)
|
||||||
|
- Clicking link opens Steam client if installed
|
||||||
|
- Falls back to Steam website if client not available
|
||||||
|
- Opens in new browser tab
|
||||||
|
|
||||||
|
### Mobile (iOS/Android)
|
||||||
|
- Clicking link opens Steam app if installed
|
||||||
|
- Falls back to mobile Steam website
|
||||||
|
- Seamless transition from browser to app
|
||||||
|
|
||||||
|
### Steam Client
|
||||||
|
- Direct deep link to trade offer page
|
||||||
|
- No need to navigate through menus
|
||||||
|
- Instant access to specific trade
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔒 Security Features
|
||||||
|
|
||||||
|
### Link Validation
|
||||||
|
- ✅ Links generated server-side (not user input)
|
||||||
|
- ✅ Offer ID comes from Steam bot response
|
||||||
|
- ✅ No risk of link manipulation
|
||||||
|
|
||||||
|
### Safe External Links
|
||||||
|
- ✅ `target="_blank"` - Opens in new tab
|
||||||
|
- ✅ `rel="noopener noreferrer"` - Security best practice
|
||||||
|
- ✅ Only official Steam domain
|
||||||
|
|
||||||
|
### Verification Code
|
||||||
|
- ✅ Code shown on site AND in Steam trade message
|
||||||
|
- ✅ User must verify codes match
|
||||||
|
- ✅ Prevents accepting wrong/fake trades
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Database Schema
|
||||||
|
|
||||||
|
### Trade Model
|
||||||
|
|
||||||
|
**Field:** `tradeOfferUrl`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
tradeOfferUrl: {
|
||||||
|
type: String,
|
||||||
|
required: false, // Optional for backwards compatibility
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example Document:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"_id": "507f1f77bcf86cd799439011",
|
||||||
|
"offerId": "7182364598",
|
||||||
|
"tradeOfferUrl": "https://steamcommunity.com/tradeoffer/7182364598",
|
||||||
|
"verificationCode": "A3X9K2",
|
||||||
|
"userId": "...",
|
||||||
|
"steamId": "...",
|
||||||
|
"state": "pending",
|
||||||
|
"items": [...],
|
||||||
|
"totalValue": 75.50
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 Testing
|
||||||
|
|
||||||
|
### Development Mode
|
||||||
|
|
||||||
|
**Mock Trade URL:**
|
||||||
|
```javascript
|
||||||
|
const mockOfferId = `DEV_${Date.now()}`;
|
||||||
|
const mockTradeOfferUrl = `https://steamcommunity.com/tradeoffer/${mockOfferId}`;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Test Flow:**
|
||||||
|
1. Set `BYPASS_BOT_REQUIREMENT=true`
|
||||||
|
2. Create trade on Sell page
|
||||||
|
3. Verify button appears in modal
|
||||||
|
4. Click "Open Trade in Steam"
|
||||||
|
5. Link opens (will show Steam error for mock ID, that's expected)
|
||||||
|
6. Check Transactions page
|
||||||
|
7. Verify "Open" button appears
|
||||||
|
8. Click "Details" - verify button in modal
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Production Mode
|
||||||
|
|
||||||
|
**Real Trade URL:**
|
||||||
|
```javascript
|
||||||
|
const tradeOfferUrl = `https://steamcommunity.com/tradeoffer/${offer.id}`;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Test Flow:**
|
||||||
|
1. Configure real Steam bot
|
||||||
|
2. Create actual trade
|
||||||
|
3. Click "Open Trade in Steam"
|
||||||
|
4. Steam client/website opens
|
||||||
|
5. Navigate to correct trade offer
|
||||||
|
6. Verify code matches
|
||||||
|
7. Accept trade
|
||||||
|
8. Verify trade completes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 UI/UX Design
|
||||||
|
|
||||||
|
### Button Styles
|
||||||
|
|
||||||
|
**Primary Button (Full Width):**
|
||||||
|
```css
|
||||||
|
- Background: Gradient (cyan → blue)
|
||||||
|
- Text: Dark surface color (high contrast)
|
||||||
|
- Icon: External link (5x5)
|
||||||
|
- Padding: 1.5rem vertical
|
||||||
|
- Border radius: 0.5rem
|
||||||
|
- Hover: 90% opacity
|
||||||
|
- Full width responsive
|
||||||
|
```
|
||||||
|
|
||||||
|
**Compact Button (Inline):**
|
||||||
|
```css
|
||||||
|
- Background: Solid primary
|
||||||
|
- Text: Dark surface color
|
||||||
|
- Icon: External link (4x4)
|
||||||
|
- Padding: 0.5rem vertical
|
||||||
|
- Border radius: 0.5rem
|
||||||
|
- Hover: Darker primary
|
||||||
|
- Fixed width (auto)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Placement Strategy
|
||||||
|
|
||||||
|
1. **Sell Modal:** After verification code (most important action)
|
||||||
|
2. **Pending List:** In action buttons (quick access)
|
||||||
|
3. **Details Modal:** Above trade info (prominent but not blocking)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Benefits
|
||||||
|
|
||||||
|
### User Experience
|
||||||
|
- ✅ **One-click access** to Steam trade
|
||||||
|
- ✅ **No manual searching** through trade offers
|
||||||
|
- ✅ **Direct deep linking** to specific trade
|
||||||
|
- ✅ **Mobile-friendly** (opens Steam app)
|
||||||
|
- ✅ **Multiple access points** (modal, list, details)
|
||||||
|
|
||||||
|
### Conversion Rate
|
||||||
|
- ✅ **Reduces friction** in trade acceptance
|
||||||
|
- ✅ **Faster completion** time
|
||||||
|
- ✅ **Less user confusion** about where to go
|
||||||
|
- ✅ **Higher acceptance rate** expected
|
||||||
|
|
||||||
|
### Support
|
||||||
|
- ✅ **Fewer support tickets** ("Where's my trade?")
|
||||||
|
- ✅ **Easier troubleshooting** (direct links to trades)
|
||||||
|
- ✅ **Better user guidance** (clear call-to-action)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 User Instructions
|
||||||
|
|
||||||
|
### For Users:
|
||||||
|
|
||||||
|
**When selling items:**
|
||||||
|
|
||||||
|
1. Click "Sell Selected Items"
|
||||||
|
2. Review and confirm
|
||||||
|
3. **Look for the big blue button** that says "Open Trade in Steam"
|
||||||
|
4. Click the button
|
||||||
|
5. Steam will open showing your trade
|
||||||
|
6. **Check the verification code** matches
|
||||||
|
7. Accept the trade
|
||||||
|
|
||||||
|
**If you close the modal:**
|
||||||
|
|
||||||
|
1. Go to "Transactions" page
|
||||||
|
2. Find your pending trade (yellow box at top)
|
||||||
|
3. Click the **"Open" button** to view trade
|
||||||
|
4. Or click "Details" for full information
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ Troubleshooting
|
||||||
|
|
||||||
|
### Link doesn't open Steam
|
||||||
|
**Issue:** Clicking link does nothing or opens browser
|
||||||
|
|
||||||
|
**Solutions:**
|
||||||
|
- Install Steam client
|
||||||
|
- Update Steam client to latest version
|
||||||
|
- Check browser allows opening external apps
|
||||||
|
- Try copying link and pasting in Steam chat
|
||||||
|
|
||||||
|
### Link opens but shows error
|
||||||
|
**Issue:** Steam shows "Invalid Trade Offer"
|
||||||
|
|
||||||
|
**Causes:**
|
||||||
|
- Trade was already accepted/declined/expired
|
||||||
|
- Bot cancelled the trade
|
||||||
|
- Network/Steam API issues
|
||||||
|
|
||||||
|
**Solutions:**
|
||||||
|
- Refresh page to check trade status
|
||||||
|
- Check if trade is still pending
|
||||||
|
- Contact support if issue persists
|
||||||
|
|
||||||
|
### Mobile app doesn't open
|
||||||
|
**Issue:** Link opens browser instead of app
|
||||||
|
|
||||||
|
**Solutions:**
|
||||||
|
- Install Steam Mobile App
|
||||||
|
- Enable "Open in app" in browser settings
|
||||||
|
- Try long-press → "Open in Steam App"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Future Enhancements
|
||||||
|
|
||||||
|
**Potential Improvements:**
|
||||||
|
|
||||||
|
- [ ] Copy link button (clipboard)
|
||||||
|
- [ ] QR code for mobile scanning
|
||||||
|
- [ ] Countdown timer until trade expires
|
||||||
|
- [ ] "Trade not showing up?" help link
|
||||||
|
- [ ] Share trade link (for admin support)
|
||||||
|
- [ ] Track link click analytics
|
||||||
|
- [ ] Mobile app deep linking optimization
|
||||||
|
- [ ] Browser extension auto-open support
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Related Documentation
|
||||||
|
|
||||||
|
- **TRADE_WORKFLOW.md** - Complete trade system flow
|
||||||
|
- **TRADE_SETUP.md** - Setup and configuration
|
||||||
|
- **TRADE_UI_COMPLETE.md** - UI features and design
|
||||||
|
- **STEAM_BOT_SETUP.md** - Bot configuration
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Status
|
||||||
|
|
||||||
|
**Status:** ✅ Complete and Live
|
||||||
|
|
||||||
|
**Added:** 2024
|
||||||
|
**Last Updated:** 2024
|
||||||
|
|
||||||
|
**Platforms Tested:**
|
||||||
|
- ✅ Desktop (Windows, macOS, Linux)
|
||||||
|
- ✅ Mobile (iOS Safari, Android Chrome)
|
||||||
|
- ✅ Steam Client (Desktop)
|
||||||
|
- ✅ Steam Mobile App (iOS, Android)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Summary:** Trade links are now fully integrated throughout the platform, providing users with quick, one-click access to their Steam trade offers from multiple locations in the UI.
|
||||||
329
TRADE_SETUP.md
Normal file
329
TRADE_SETUP.md
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
# Trade System Setup Guide
|
||||||
|
|
||||||
|
## Quick Start (Development Mode - No Steam Bots Required)
|
||||||
|
|
||||||
|
For testing the trade system without real Steam bots:
|
||||||
|
|
||||||
|
### 1. Enable Bypass Mode
|
||||||
|
|
||||||
|
Add to your `.env` file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
NODE_ENV=development
|
||||||
|
BYPASS_BOT_REQUIREMENT=true
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Restart Backend
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Test the Flow
|
||||||
|
|
||||||
|
1. Go to `/sell` page
|
||||||
|
2. Select items to sell
|
||||||
|
3. Click "Sell Selected Items"
|
||||||
|
4. You'll get a mock trade with verification code
|
||||||
|
5. **To complete the trade and credit balance:**
|
||||||
|
```bash
|
||||||
|
# Get the trade ID from the response, then:
|
||||||
|
curl -X POST http://localhost:3000/api/inventory/trade/TRADE_ID/complete \
|
||||||
|
-H "Cookie: accessToken=YOUR_TOKEN"
|
||||||
|
```
|
||||||
|
Or use the frontend to call: `POST /api/inventory/trade/:tradeId/complete`
|
||||||
|
|
||||||
|
### 4. Check Balance
|
||||||
|
|
||||||
|
Your balance should be credited automatically!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Production Setup (With Real Steam Bots)
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
You need:
|
||||||
|
- ✅ Steam account(s) for bots
|
||||||
|
- ✅ Steam Mobile Authenticator enabled on each bot account
|
||||||
|
- ✅ `shared_secret` and `identity_secret` for each bot
|
||||||
|
- ✅ Steam API Key ([get one here](https://steamcommunity.com/dev/apikey))
|
||||||
|
- ⚠️ Optional: SOCKS5/HTTP proxies (recommended for multiple bots)
|
||||||
|
|
||||||
|
### Step 1: Extract Bot Secrets
|
||||||
|
|
||||||
|
#### Using [SDA (Steam Desktop Authenticator)](https://github.com/Jessecar96/SteamDesktopAuthenticator):
|
||||||
|
|
||||||
|
1. Install SDA on your computer
|
||||||
|
2. Add your bot account to SDA
|
||||||
|
3. Navigate to SDA's data folder:
|
||||||
|
- Windows: `%APPDATA%\SteamDesktopAuthenticator`
|
||||||
|
- Linux: `~/.config/SteamDesktopAuthenticator`
|
||||||
|
4. Open `maFiles/<steamid>.maFile`
|
||||||
|
5. Copy `shared_secret` and `identity_secret`
|
||||||
|
|
||||||
|
#### Using [steam-totp](https://www.npmjs.com/package/steam-totp):
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// If you have your Steam Guard secret:
|
||||||
|
import SteamTotp from 'steam-totp';
|
||||||
|
const code = SteamTotp.generateAuthCode('YOUR_SHARED_SECRET');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Create Bot Configuration
|
||||||
|
|
||||||
|
Create `config/steam-bots.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"username": "turbotrades_bot1",
|
||||||
|
"password": "your_steam_password",
|
||||||
|
"sharedSecret": "abcd1234efgh5678ijkl==",
|
||||||
|
"identitySecret": "wxyz9876vuts5432pqrs==",
|
||||||
|
"steamApiKey": "YOUR_STEAM_API_KEY",
|
||||||
|
"pollInterval": 30000,
|
||||||
|
"tradeTimeout": 600000,
|
||||||
|
"proxy": {
|
||||||
|
"type": "socks5",
|
||||||
|
"host": "proxy.example.com",
|
||||||
|
"port": 1080,
|
||||||
|
"username": "proxy_user",
|
||||||
|
"password": "proxy_password"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Notes:**
|
||||||
|
- `proxy` is optional but recommended for multiple bots
|
||||||
|
- `pollInterval`: How often to check for trade updates (ms)
|
||||||
|
- `tradeTimeout`: How long before trade auto-cancels (ms)
|
||||||
|
|
||||||
|
### Step 3: Enable Auto-Start
|
||||||
|
|
||||||
|
Add to `.env`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
STEAM_BOT_AUTO_START=true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Start Backend
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
You should see:
|
||||||
|
```
|
||||||
|
🤖 Auto-starting Steam bots...
|
||||||
|
✅ Bot turbotrades_bot1 ready
|
||||||
|
✅ 1/1 bots initialized successfully
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 5: Test Trade Flow
|
||||||
|
|
||||||
|
1. Set your trade URL in profile (`/profile`)
|
||||||
|
2. Go to sell page (`/sell`)
|
||||||
|
3. Select items
|
||||||
|
4. Create trade offer
|
||||||
|
5. Check Steam for trade offer
|
||||||
|
6. Verify code matches
|
||||||
|
7. Accept trade in Steam
|
||||||
|
8. Balance credited automatically!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Manual Bot Initialization (Alternative)
|
||||||
|
|
||||||
|
If you don't want auto-start, you can initialize bots via API:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// In your code or via admin endpoint
|
||||||
|
import { getSteamBotManager } from './services/steamBot.js';
|
||||||
|
|
||||||
|
const botManager = getSteamBotManager();
|
||||||
|
|
||||||
|
const botsConfig = [
|
||||||
|
{
|
||||||
|
username: "bot1",
|
||||||
|
password: "pass",
|
||||||
|
sharedSecret: "secret",
|
||||||
|
identitySecret: "secret"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
await botManager.initialize(botsConfig);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Environment Variables Reference
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Development Mode (bypass bots)
|
||||||
|
NODE_ENV=development
|
||||||
|
BYPASS_BOT_REQUIREMENT=true
|
||||||
|
|
||||||
|
# Production Mode (real bots)
|
||||||
|
NODE_ENV=production
|
||||||
|
STEAM_BOT_AUTO_START=true
|
||||||
|
STEAM_APIS_KEY=your_steam_api_key
|
||||||
|
|
||||||
|
# Optional
|
||||||
|
ENABLE_PRICE_UPDATES=true
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verification Codes
|
||||||
|
|
||||||
|
- **Format**: 6 alphanumeric characters (e.g., `A3X9K2`)
|
||||||
|
- **Purpose**: Prevent phishing attacks
|
||||||
|
- **How it works**:
|
||||||
|
1. Code shown on website
|
||||||
|
2. Code included in Steam trade message
|
||||||
|
3. User must verify codes match before accepting
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## WebSocket Events (Real-time Updates)
|
||||||
|
|
||||||
|
Your frontend will receive these events:
|
||||||
|
|
||||||
|
- `trade_creating` - Trade is being created
|
||||||
|
- `trade_sent` - Trade sent to Steam
|
||||||
|
- `trade_confirmed` - Trade confirmed with 2FA
|
||||||
|
- `trade_created` - Trade ready (includes verification code)
|
||||||
|
- `trade_accepted` - User accepted on Steam
|
||||||
|
- `trade_completed` - Balance credited
|
||||||
|
- `balance_update` - Balance changed
|
||||||
|
- `trade_declined` - User declined
|
||||||
|
- `trade_expired` - Trade expired
|
||||||
|
- `trade_canceled` - Trade canceled
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Monitoring
|
||||||
|
|
||||||
|
### Check Bot Health
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Via admin endpoint (requires admin role)
|
||||||
|
curl http://localhost:3000/api/admin/bots/health
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check Bot Stats
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { getSteamBotManager } from './services/steamBot.js';
|
||||||
|
|
||||||
|
const botManager = getSteamBotManager();
|
||||||
|
const stats = botManager.getStats();
|
||||||
|
|
||||||
|
console.log(stats);
|
||||||
|
// {
|
||||||
|
// totalBots: 2,
|
||||||
|
// healthyBots: 2,
|
||||||
|
// totalTrades: 15,
|
||||||
|
// totalActiveTrades: 3,
|
||||||
|
// totalErrors: 0
|
||||||
|
// }
|
||||||
|
```
|
||||||
|
|
||||||
|
### View Trade History
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:3000/api/inventory/trades \
|
||||||
|
-H "Cookie: accessToken=YOUR_TOKEN"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "Trade system unavailable"
|
||||||
|
|
||||||
|
**Cause**: Bots not initialized
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
- Development: Set `BYPASS_BOT_REQUIREMENT=true`
|
||||||
|
- Production: Check bot config and set `STEAM_BOT_AUTO_START=true`
|
||||||
|
|
||||||
|
### "Bot login failed"
|
||||||
|
|
||||||
|
**Causes**:
|
||||||
|
- Wrong username/password
|
||||||
|
- Wrong shared_secret
|
||||||
|
- Steam Guard not enabled
|
||||||
|
- Account locked/banned
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
1. Verify credentials
|
||||||
|
2. Test login manually via Steam client
|
||||||
|
3. Check bot account is not limited (spent $5+ on Steam)
|
||||||
|
|
||||||
|
### "Confirmation failed"
|
||||||
|
|
||||||
|
**Cause**: Wrong `identity_secret`
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
- Double-check identity_secret from SDA maFile
|
||||||
|
- Ensure mobile auth is enabled
|
||||||
|
|
||||||
|
### Trade created but not appearing in Steam
|
||||||
|
|
||||||
|
**Causes**:
|
||||||
|
- User's trade URL is incorrect
|
||||||
|
- User's inventory is private
|
||||||
|
- Items became untradable
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
1. Verify trade URL format
|
||||||
|
2. Make inventory public
|
||||||
|
3. Check item trade restrictions
|
||||||
|
|
||||||
|
### Balance not credited after accepting trade
|
||||||
|
|
||||||
|
**Causes**:
|
||||||
|
- Backend event listener not working
|
||||||
|
- Database error
|
||||||
|
- WebSocket disconnected
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
1. Check backend logs for `tradeAccepted` event
|
||||||
|
2. Check Trade status in database
|
||||||
|
3. Manually complete via: `POST /api/inventory/trade/:tradeId/complete` (dev only)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security Best Practices
|
||||||
|
|
||||||
|
1. ✅ **Never expose bot credentials** - Store in secure config, not in code
|
||||||
|
2. ✅ **Use proxies** - Distribute bot IPs to avoid rate limits
|
||||||
|
3. ✅ **Monitor bot health** - Set up alerts for bot failures
|
||||||
|
4. ✅ **Verification codes** - Always show and require verification
|
||||||
|
5. ✅ **Rate limiting** - Limit trades per user per hour
|
||||||
|
6. ✅ **Escrow handling** - Warn users about 7-day trade holds
|
||||||
|
7. ✅ **Audit logs** - Log all trade events for debugging
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API Endpoints Summary
|
||||||
|
|
||||||
|
| Method | Endpoint | Description |
|
||||||
|
|--------|----------|-------------|
|
||||||
|
| POST | `/api/inventory/sell` | Create trade offer |
|
||||||
|
| GET | `/api/inventory/trades` | Get trade history |
|
||||||
|
| GET | `/api/inventory/trade/:id` | Get trade details |
|
||||||
|
| POST | `/api/inventory/trade/:id/cancel` | Cancel pending trade |
|
||||||
|
| POST | `/api/inventory/trade/:id/complete` | Complete trade (dev only) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Need Help?
|
||||||
|
|
||||||
|
- 📖 Read `TRADE_WORKFLOW.md` for detailed flow documentation
|
||||||
|
- 🤖 Read `STEAM_BOT_SETUP.md` for bot setup details
|
||||||
|
- 🔧 Check logs in backend console
|
||||||
|
- 💬 Check WebSocket messages in browser dev tools
|
||||||
418
TRADE_UI_COMPLETE.md
Normal file
418
TRADE_UI_COMPLETE.md
Normal file
@@ -0,0 +1,418 @@
|
|||||||
|
# Trade UI Complete - Summary
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The trade system now has a complete, polished UI with real-time WebSocket updates, 2FA-style modals, and pending trade management across the platform.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 New Features
|
||||||
|
|
||||||
|
### 1. Trade Status Modal (Sell Page)
|
||||||
|
|
||||||
|
A comprehensive modal that guides users through the entire trade process with 4 distinct states:
|
||||||
|
|
||||||
|
#### **State 1: Confirming**
|
||||||
|
- Shows selected items summary
|
||||||
|
- Displays total value
|
||||||
|
- "You Will Receive" amount
|
||||||
|
- Important instructions about Steam trade offers
|
||||||
|
- Cancel / Confirm Sale buttons
|
||||||
|
|
||||||
|
#### **State 2: Created** (Trade Offer Sent)
|
||||||
|
- ✅ Success indicator with checkmark icon
|
||||||
|
- **Large verification code display** (4xl, bold, mono font)
|
||||||
|
- Trade details (items, value, status)
|
||||||
|
- Step-by-step instructions:
|
||||||
|
1. Open Steam
|
||||||
|
2. Go to trade offers
|
||||||
|
3. Find TurboTrades offer
|
||||||
|
4. Verify code matches
|
||||||
|
5. Accept trade
|
||||||
|
- Modal stays open until user closes it
|
||||||
|
- Code is shown in yellow/primary color for visibility
|
||||||
|
|
||||||
|
#### **State 3: Accepted** (Trade Complete)
|
||||||
|
- ✅ Green success indicator
|
||||||
|
- "Trade Complete!" message
|
||||||
|
- Amount credited display
|
||||||
|
- New balance display
|
||||||
|
- "Done" button to close
|
||||||
|
|
||||||
|
#### **State 4: Error**
|
||||||
|
- ❌ Red error indicator
|
||||||
|
- Error message display
|
||||||
|
- "Close" button to retry
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Pending Trades Section (Transactions Page)
|
||||||
|
|
||||||
|
New section at the top of the transactions page showing all pending trades:
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- Yellow highlight/border for visibility
|
||||||
|
- Pulsing dot indicator for active status
|
||||||
|
- Shows item count
|
||||||
|
- **Verification code displayed inline** (mono font, primary color)
|
||||||
|
- Total value shown
|
||||||
|
- "View Details" button to open full modal
|
||||||
|
|
||||||
|
**Auto-updates:**
|
||||||
|
- ✅ Removed when trade accepted
|
||||||
|
- ✅ Removed when trade declined
|
||||||
|
- ✅ Removed when trade expired
|
||||||
|
- ✅ Removed when trade cancelled
|
||||||
|
- ✅ Added when new trade created
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Trade Details Modal (Transactions Page)
|
||||||
|
|
||||||
|
When clicking "View Details" on a pending trade:
|
||||||
|
|
||||||
|
**Shows:**
|
||||||
|
- ⏰ Waiting indicator with clock icon
|
||||||
|
- **Large verification code** (same style as sell modal)
|
||||||
|
- Trade information:
|
||||||
|
- Items count
|
||||||
|
- Value
|
||||||
|
- Created date
|
||||||
|
- Step-by-step instructions
|
||||||
|
- "Close" button
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Real-Time Updates (WebSocket)
|
||||||
|
|
||||||
|
Both pages now listen to WebSocket events and update automatically:
|
||||||
|
|
||||||
|
### Events Handled:
|
||||||
|
|
||||||
|
| Event | Sell Page | Transactions Page |
|
||||||
|
|-------|-----------|-------------------|
|
||||||
|
| `trade_created` | Updates modal to show code | Adds to pending list |
|
||||||
|
| `trade_completed` | Updates modal to "accepted" state | Removes from pending, refreshes list |
|
||||||
|
| `trade_declined` | Shows error state | Removes from pending |
|
||||||
|
| `trade_expired` | Shows error state | Removes from pending |
|
||||||
|
| `trade_canceled` | N/A | Removes from pending |
|
||||||
|
| `balance_update` | Updates auth store balance | Updates auth store balance |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 User Experience Flow
|
||||||
|
|
||||||
|
### Complete Sell Journey:
|
||||||
|
|
||||||
|
1. User selects items on `/sell` page
|
||||||
|
2. User clicks "Sell Selected Items"
|
||||||
|
3. **Modal opens** - State: Confirming
|
||||||
|
4. User reviews and clicks "Confirm Sale"
|
||||||
|
5. **Modal updates** - State: Created
|
||||||
|
- Shows verification code immediately
|
||||||
|
- Modal stays open
|
||||||
|
- Items removed from selection
|
||||||
|
6. User opens Steam on phone/desktop
|
||||||
|
7. User finds trade offer from TurboTrades bot
|
||||||
|
8. User verifies code matches the one shown in modal
|
||||||
|
9. User accepts trade in Steam
|
||||||
|
10. **Modal auto-updates** - State: Accepted
|
||||||
|
- Shows success message
|
||||||
|
- Shows amount credited
|
||||||
|
- Shows new balance
|
||||||
|
11. User clicks "Done"
|
||||||
|
12. User can continue shopping or selling more items
|
||||||
|
|
||||||
|
### Tracking Pending Trades:
|
||||||
|
|
||||||
|
1. User navigates to `/transactions`
|
||||||
|
2. **Pending Trades section** shows at top (if any pending)
|
||||||
|
3. User sees verification code for each pending trade
|
||||||
|
4. User can click "View Details" for full instructions
|
||||||
|
5. Section updates in real-time as trades complete
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💾 Data Flow
|
||||||
|
|
||||||
|
### Sell Endpoint (`POST /api/inventory/sell`):
|
||||||
|
|
||||||
|
**Development Mode** (`BYPASS_BOT_REQUIREMENT=true`):
|
||||||
|
```
|
||||||
|
1. Validate items & trade URL
|
||||||
|
2. Generate mock verification code (6 chars)
|
||||||
|
3. Create Trade record in database
|
||||||
|
4. Send WebSocket notification (trade_created)
|
||||||
|
5. Return trade details to frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
**Production Mode** (with Steam bots):
|
||||||
|
```
|
||||||
|
1. Validate items & trade URL
|
||||||
|
2. Get best available bot
|
||||||
|
3. Bot generates verification code
|
||||||
|
4. Bot creates Steam trade offer
|
||||||
|
5. Bot confirms with 2FA
|
||||||
|
6. Create Trade record in database
|
||||||
|
7. Send WebSocket notifications (trade_creating, trade_sent, trade_confirmed, trade_created)
|
||||||
|
8. Return trade details to frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
### Trade Completion Flow:
|
||||||
|
|
||||||
|
**User accepts trade in Steam:**
|
||||||
|
```
|
||||||
|
1. Steam API notifies bot
|
||||||
|
2. Bot emits tradeAccepted event
|
||||||
|
3. Backend event listener catches it
|
||||||
|
4. Update Trade.state = "accepted"
|
||||||
|
5. Credit user balance
|
||||||
|
6. Create Transaction record
|
||||||
|
7. Send WebSocket notifications (trade_accepted, trade_completed, balance_update)
|
||||||
|
8. Frontend auto-updates modal & removes from pending list
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Technical Implementation
|
||||||
|
|
||||||
|
### Frontend Components:
|
||||||
|
|
||||||
|
**SellPage.vue:**
|
||||||
|
- Added `tradeState` ref (confirming, created, accepted, error)
|
||||||
|
- Added `currentTrade` ref (stores trade data)
|
||||||
|
- Added `showTradeModal` ref (controls modal visibility)
|
||||||
|
- Added WebSocket message handler
|
||||||
|
- Modal persists through states
|
||||||
|
- Real-time updates via WebSocket
|
||||||
|
|
||||||
|
**TransactionsPage.vue:**
|
||||||
|
- Added `pendingTrades` ref (array of pending trades)
|
||||||
|
- Added `showTradeModal` ref (for detail view)
|
||||||
|
- Added `selectedTrade` ref (trade being viewed)
|
||||||
|
- Added WebSocket message handler
|
||||||
|
- Fetches pending trades on mount
|
||||||
|
- Real-time updates via WebSocket
|
||||||
|
|
||||||
|
### Backend Changes:
|
||||||
|
|
||||||
|
**routes/inventory.js:**
|
||||||
|
- Fixed Trade model field names (userId, steamId, userReceives, state, assetId, game)
|
||||||
|
- Added development mode bypass with mock trades
|
||||||
|
- Added `/api/inventory/trade/:tradeId/complete` endpoint (dev only)
|
||||||
|
- Fixed all trade queries to use correct field names
|
||||||
|
|
||||||
|
**index.js:**
|
||||||
|
- Fixed trade event listeners to use correct field names
|
||||||
|
- Trade acceptance now properly credits balance
|
||||||
|
- Creates transaction records
|
||||||
|
- Sends WebSocket notifications
|
||||||
|
|
||||||
|
**services/steamBot.js:**
|
||||||
|
- Added WebSocket notifications throughout trade lifecycle
|
||||||
|
- Sends real-time updates to users
|
||||||
|
- Includes userId in trade data for WebSocket routing
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 Design Features
|
||||||
|
|
||||||
|
### Verification Code Display:
|
||||||
|
- **Font:** Monospace (font-mono)
|
||||||
|
- **Size:** 4xl (very large, ~2.25rem)
|
||||||
|
- **Color:** Primary color (cyan/blue gradient)
|
||||||
|
- **Style:** Bold, uppercase, letter-spaced
|
||||||
|
- **Background:** Gradient from primary/20 to primary-dark/20
|
||||||
|
- **Border:** 2px solid primary color
|
||||||
|
- **Purpose:** Unmissable, easy to read and compare
|
||||||
|
|
||||||
|
### Modal States:
|
||||||
|
- **Confirming:** Blue/primary theme
|
||||||
|
- **Created:** Yellow/pending theme with primary code
|
||||||
|
- **Accepted:** Green/success theme
|
||||||
|
- **Error:** Red/danger theme
|
||||||
|
|
||||||
|
### Pending Trades List:
|
||||||
|
- Yellow border and background tint
|
||||||
|
- Pulsing dot indicator
|
||||||
|
- Inline code display
|
||||||
|
- Hover effects on buttons
|
||||||
|
- Responsive grid layout
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Environment Variables
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Development Mode (bypass bots, create mock trades)
|
||||||
|
BYPASS_BOT_REQUIREMENT=true
|
||||||
|
|
||||||
|
# Production Mode (use real Steam bots)
|
||||||
|
STEAM_BOT_AUTO_START=true
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 Testing
|
||||||
|
|
||||||
|
### Test Mock Trade Flow:
|
||||||
|
|
||||||
|
1. Set `BYPASS_BOT_REQUIREMENT=true` in `.env`
|
||||||
|
2. Restart backend
|
||||||
|
3. Go to `/sell` page
|
||||||
|
4. Select items and click "Sell Selected Items"
|
||||||
|
5. Modal opens - review and confirm
|
||||||
|
6. **Verification code appears in modal**
|
||||||
|
7. Modal stays open
|
||||||
|
8. Check `/transactions` page - trade appears in pending section
|
||||||
|
9. Manually complete trade:
|
||||||
|
```bash
|
||||||
|
POST /api/inventory/trade/TRADE_ID/complete
|
||||||
|
```
|
||||||
|
10. Modal auto-updates to "accepted" state
|
||||||
|
11. Pending trade removed from transactions page
|
||||||
|
12. Balance credited
|
||||||
|
13. Transaction appears in history
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Production Deployment
|
||||||
|
|
||||||
|
### With Real Steam Bots:
|
||||||
|
|
||||||
|
1. Create Steam bot accounts
|
||||||
|
2. Enable Steam Mobile Authenticator
|
||||||
|
3. Extract shared_secret and identity_secret
|
||||||
|
4. Create `config/steam-bots.json`
|
||||||
|
5. Set `STEAM_BOT_AUTO_START=true`
|
||||||
|
6. Remove `BYPASS_BOT_REQUIREMENT` (or set to false)
|
||||||
|
7. Restart backend
|
||||||
|
8. Bots will initialize automatically
|
||||||
|
9. Full trade flow works with real Steam offers
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Completed Features
|
||||||
|
|
||||||
|
- [x] 2FA-style modal for trade creation
|
||||||
|
- [x] Large, prominent verification code display
|
||||||
|
- [x] Multi-state modal (confirming → created → accepted/error)
|
||||||
|
- [x] Real-time WebSocket updates
|
||||||
|
- [x] Pending trades section on transactions page
|
||||||
|
- [x] Trade details modal from transactions page
|
||||||
|
- [x] Auto-remove pending trades when completed/cancelled
|
||||||
|
- [x] Balance updates in real-time
|
||||||
|
- [x] Toast notifications for trade events
|
||||||
|
- [x] Development mode with mock trades
|
||||||
|
- [x] Production mode with real Steam bots
|
||||||
|
- [x] Responsive design for mobile/desktop
|
||||||
|
- [x] Error handling and user feedback
|
||||||
|
- [x] Instructions for users throughout flow
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Next Steps
|
||||||
|
|
||||||
|
**Optional Enhancements:**
|
||||||
|
- [ ] Copy verification code button
|
||||||
|
- [ ] QR code for mobile Steam app
|
||||||
|
- [ ] Push notifications when trade is ready
|
||||||
|
- [ ] Trade timeout countdown timer
|
||||||
|
- [ ] Trade history page with filters
|
||||||
|
- [ ] Admin panel for monitoring trades
|
||||||
|
- [ ] Automatic retry on trade failure
|
||||||
|
- [ ] Trade offer preview with item images
|
||||||
|
- [ ] Email notifications for trade status
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📸 UI States
|
||||||
|
|
||||||
|
### Sell Page Modal - State 1 (Confirming)
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────┐
|
||||||
|
│ Confirm Sale [X] │
|
||||||
|
├─────────────────────────────────┤
|
||||||
|
│ You're about to sell 3 items... │
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────────────────────┐ │
|
||||||
|
│ │ Items Selected: 3 │ │
|
||||||
|
│ │ Total Value: $75.50 │ │
|
||||||
|
│ │ ─────────────────────────── │ │
|
||||||
|
│ │ You Will Receive: $75.50 │ │
|
||||||
|
│ └─────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ [!] Important: You will receive │
|
||||||
|
│ a Steam trade offer... │
|
||||||
|
│ │
|
||||||
|
│ [Cancel] [Confirm Sale] │
|
||||||
|
└─────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sell Page Modal - State 2 (Created)
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────┐
|
||||||
|
│ Trade Offer Created! [X] │
|
||||||
|
├─────────────────────────────────┤
|
||||||
|
│ ┌─────┐ │
|
||||||
|
│ │ ✓ │ │
|
||||||
|
│ └─────┘ │
|
||||||
|
│ Trade Offer Created! │
|
||||||
|
│ Check your Steam │
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────────────────────┐ │
|
||||||
|
│ │ Verification Code │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ A3X9K2 │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ Match this code with your │ │
|
||||||
|
│ │ Steam trade offer │ │
|
||||||
|
│ └─────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ Items: 3 Value: $75.50 │
|
||||||
|
│ Status: ⏳ Pending │
|
||||||
|
│ │
|
||||||
|
│ Next Steps: │
|
||||||
|
│ 1. Open Steam │
|
||||||
|
│ 2. Go to trade offers │
|
||||||
|
│ 3. Find offer from TurboTrades │
|
||||||
|
│ 4. Verify code: A3X9K2 │
|
||||||
|
│ 5. Accept the trade │
|
||||||
|
│ │
|
||||||
|
│ [Close] │
|
||||||
|
└─────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Transactions Page - Pending Trades
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────┐
|
||||||
|
│ ⏰ Pending Trades │
|
||||||
|
├────────────────────────────────────────┤
|
||||||
|
│ ┌────────────────────────────────────┐ │
|
||||||
|
│ │ ● Selling 3 item(s) │ Code: A3X9K2││ │
|
||||||
|
│ │ $75.50 [View Details] │ │
|
||||||
|
│ └────────────────────────────────────┘ │
|
||||||
|
└────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 Support
|
||||||
|
|
||||||
|
**Development Issues:**
|
||||||
|
- Check `BYPASS_BOT_REQUIREMENT` is set to `true`
|
||||||
|
- Check backend logs for errors
|
||||||
|
- Check browser console for WebSocket messages
|
||||||
|
- Verify trade URL is set in profile
|
||||||
|
|
||||||
|
**Production Issues:**
|
||||||
|
- Check bot status: `GET /api/admin/bots/health`
|
||||||
|
- Check bot logs for login errors
|
||||||
|
- Verify shared_secret and identity_secret are correct
|
||||||
|
- Check Steam API rate limits
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Status:** ✅ Complete and Ready for Testing
|
||||||
|
**Last Updated:** 2024
|
||||||
|
**Documentation:** `TRADE_WORKFLOW.md`, `TRADE_SETUP.md`
|
||||||
445
TRADE_WORKFLOW.md
Normal file
445
TRADE_WORKFLOW.md
Normal file
@@ -0,0 +1,445 @@
|
|||||||
|
# TurboTrades - Steam Trade Workflow Documentation
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This document explains the complete workflow for selling items through Steam trade offers with real-time WebSocket updates and verification codes.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
User (Frontend) <--WebSocket--> Backend <--> Steam Bot Manager <--> Steam API
|
||||||
|
|
|
||||||
|
v
|
||||||
|
Database
|
||||||
|
(Trades, Users, Transactions)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Complete Trade Flow
|
||||||
|
|
||||||
|
### 1. User Initiates Sale (Frontend)
|
||||||
|
|
||||||
|
**File**: `frontend/src/views/SellPage.vue`
|
||||||
|
|
||||||
|
1. User logs in and navigates to `/sell`
|
||||||
|
2. Frontend fetches user's Steam inventory via `/api/inventory/steam`
|
||||||
|
3. Items are displayed with market prices (pre-fetched from database)
|
||||||
|
4. User selects items to sell
|
||||||
|
5. User clicks "Sell Selected Items"
|
||||||
|
6. Frontend makes POST request to `/api/inventory/sell` with:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tradeUrl": "user's Steam trade URL",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"assetid": "123456789",
|
||||||
|
"appid": 730,
|
||||||
|
"contextid": "2",
|
||||||
|
"name": "AK-47 | Redline (Field-Tested)",
|
||||||
|
"price": 25.50,
|
||||||
|
"image": "url",
|
||||||
|
"wear": "ft",
|
||||||
|
"rarity": "classified"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Backend Creates Trade Offer
|
||||||
|
|
||||||
|
**File**: `routes/inventory.js` - POST `/api/inventory/sell`
|
||||||
|
|
||||||
|
1. Validates user authentication and trade URL
|
||||||
|
2. Calculates total value of items
|
||||||
|
3. Calls `botManager.createTradeOffer()` with:
|
||||||
|
- User's trade URL
|
||||||
|
- Items to receive (assetid, appid, contextid)
|
||||||
|
- User ID (for WebSocket notifications)
|
||||||
|
4. Bot manager selects best available bot (least busy)
|
||||||
|
5. Bot manager generates 6-character verification code
|
||||||
|
6. Bot creates Steam trade offer with verification code in message
|
||||||
|
|
||||||
|
### 3. Trade Offer Created
|
||||||
|
|
||||||
|
**File**: `services/steamBot.js` - `SteamBotInstance.createTradeOffer()`
|
||||||
|
|
||||||
|
1. Bot sends trade offer to Steam
|
||||||
|
2. Bot confirms trade offer with 2FA (identity_secret)
|
||||||
|
3. Trade record saved to database with status: `pending`
|
||||||
|
4. WebSocket notifications sent to user:
|
||||||
|
|
||||||
|
**WebSocket Events Sent**:
|
||||||
|
- `trade_creating` - Trade is being created
|
||||||
|
- `trade_sent` - Trade offer sent to Steam
|
||||||
|
- `trade_confirmed` - Trade confirmed with 2FA
|
||||||
|
- `trade_created` - Complete trade details
|
||||||
|
|
||||||
|
5. Response sent to frontend with:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"trade": {
|
||||||
|
"tradeId": "db_record_id",
|
||||||
|
"offerId": "steam_offer_id",
|
||||||
|
"verificationCode": "A3X9K2",
|
||||||
|
"itemCount": 3,
|
||||||
|
"totalValue": 75.50,
|
||||||
|
"status": "pending",
|
||||||
|
"botId": "bot1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. User Views Verification Code
|
||||||
|
|
||||||
|
**File**: `frontend/src/views/SellPage.vue`
|
||||||
|
|
||||||
|
1. Toast notification shows verification code (doesn't auto-dismiss)
|
||||||
|
2. User receives WebSocket message with trade details
|
||||||
|
3. User opens Steam client/mobile app
|
||||||
|
4. User sees trade offer from TurboTrades bot
|
||||||
|
|
||||||
|
### 5. User Verifies and Accepts Trade
|
||||||
|
|
||||||
|
**Steam Side**:
|
||||||
|
1. User checks trade offer message for verification code
|
||||||
|
2. User compares code in Steam with code shown on website
|
||||||
|
3. If codes match, user accepts trade
|
||||||
|
4. If codes don't match, user declines (security protection)
|
||||||
|
|
||||||
|
### 6. Bot Detects Trade Acceptance
|
||||||
|
|
||||||
|
**File**: `services/steamBot.js` - `_handleOfferStateChange()`
|
||||||
|
|
||||||
|
When Steam trade state changes to `Accepted` (state 3):
|
||||||
|
|
||||||
|
1. Bot emits `tradeAccepted` event
|
||||||
|
2. Event includes offer details and trade data
|
||||||
|
|
||||||
|
### 7. Backend Processes Completed Trade
|
||||||
|
|
||||||
|
**File**: `index.js` - Trade event listener
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
botManager.on('tradeAccepted', async (offer, tradeData) => {
|
||||||
|
// 1. Find trade record in database
|
||||||
|
// 2. Find user record
|
||||||
|
// 3. Credit user balance with trade value
|
||||||
|
// 4. Update trade status to 'completed'
|
||||||
|
// 5. Create transaction record (type: 'sale')
|
||||||
|
// 6. Send WebSocket notifications
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**WebSocket Events Sent**:
|
||||||
|
- `trade_accepted` - Trade was accepted on Steam
|
||||||
|
- `trade_completed` - Balance credited successfully
|
||||||
|
- `balance_update` - User's new balance
|
||||||
|
|
||||||
|
### 8. User Receives Funds
|
||||||
|
|
||||||
|
1. User balance updated in database
|
||||||
|
2. Transaction record created for audit trail
|
||||||
|
3. WebSocket notifications update frontend in real-time
|
||||||
|
4. Toast notification shows credited amount
|
||||||
|
5. User can spend balance on marketplace
|
||||||
|
|
||||||
|
## WebSocket Events Reference
|
||||||
|
|
||||||
|
### Events Sent to User During Trade
|
||||||
|
|
||||||
|
| Event | Trigger | Data |
|
||||||
|
|-------|---------|------|
|
||||||
|
| `trade_creating` | Bot starting to create offer | verificationCode, itemCount, botId |
|
||||||
|
| `trade_sent` | Offer sent to Steam | offerId, verificationCode, status |
|
||||||
|
| `trade_confirmed` | Offer confirmed with 2FA | offerId, verificationCode |
|
||||||
|
| `trade_created` | Complete - offer ready | tradeId, offerId, verificationCode, totalValue |
|
||||||
|
| `trade_accepted` | User accepted on Steam | offerId, verificationCode, itemCount |
|
||||||
|
| `trade_completed` | Balance credited | tradeId, amount, newBalance |
|
||||||
|
| `balance_update` | Balance changed | balance, change, reason |
|
||||||
|
| `trade_declined` | User declined on Steam | offerId, tradeId |
|
||||||
|
| `trade_expired` | Offer expired (15 days) | offerId, tradeId |
|
||||||
|
| `trade_canceled` | User/bot canceled | offerId, tradeId |
|
||||||
|
| `trade_error` | Error occurred | error, verificationCode |
|
||||||
|
|
||||||
|
### WebSocket Message Format
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "trade_completed",
|
||||||
|
"data": {
|
||||||
|
"tradeId": "507f1f77bcf86cd799439011",
|
||||||
|
"offerId": "5847362918",
|
||||||
|
"amount": 75.50,
|
||||||
|
"newBalance": 125.75,
|
||||||
|
"itemCount": 3,
|
||||||
|
"timestamp": 1704067200000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Database Models
|
||||||
|
|
||||||
|
### Trade Model
|
||||||
|
|
||||||
|
**File**: `models/Trade.js`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
offerId: String, // Steam trade offer ID
|
||||||
|
botId: String, // Bot that created the offer
|
||||||
|
user: ObjectId, // User reference
|
||||||
|
items: [{ // Items in trade
|
||||||
|
assetid: String,
|
||||||
|
name: String,
|
||||||
|
price: Number,
|
||||||
|
image: String
|
||||||
|
}],
|
||||||
|
totalValue: Number, // Total value of items
|
||||||
|
verificationCode: String, // 6-char code
|
||||||
|
status: String, // pending, completed, declined, expired, cancelled
|
||||||
|
type: String, // sell, buy
|
||||||
|
createdAt: Date,
|
||||||
|
completedAt: Date
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Transaction Model
|
||||||
|
|
||||||
|
**File**: `models/Transaction.js`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
user: ObjectId,
|
||||||
|
type: String, // sale, deposit, withdrawal, purchase
|
||||||
|
amount: Number,
|
||||||
|
description: String,
|
||||||
|
status: String, // completed, pending, failed
|
||||||
|
metadata: {
|
||||||
|
tradeId: ObjectId,
|
||||||
|
offerId: String,
|
||||||
|
botId: String,
|
||||||
|
verificationCode: String
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Features
|
||||||
|
|
||||||
|
### Verification Codes
|
||||||
|
|
||||||
|
- **Purpose**: Prevent phishing and fake trade offers
|
||||||
|
- **Format**: 6 alphanumeric characters (no ambiguous chars: O, 0, I, l, 1)
|
||||||
|
- **Display**: Shown on website and in Steam trade message
|
||||||
|
- **Validation**: User must verify codes match before accepting
|
||||||
|
|
||||||
|
### Trade URL Privacy
|
||||||
|
|
||||||
|
- Trade URLs stored encrypted in database
|
||||||
|
- Only sent to bot manager, never exposed to frontend
|
||||||
|
- Required for creating trade offers
|
||||||
|
|
||||||
|
### Bot Security
|
||||||
|
|
||||||
|
- Each bot uses separate Steam account
|
||||||
|
- Proxies supported for IP distribution
|
||||||
|
- 2FA required (shared_secret, identity_secret)
|
||||||
|
- Rate limiting on trade creation
|
||||||
|
- Health monitoring and auto-failover
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### POST /api/inventory/sell
|
||||||
|
|
||||||
|
Create a new trade offer to sell items.
|
||||||
|
|
||||||
|
**Request**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tradeUrl": "https://steamcommunity.com/tradeoffer/new/?partner=XXX&token=YYY",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"assetid": "123456789",
|
||||||
|
"appid": 730,
|
||||||
|
"contextid": "2",
|
||||||
|
"name": "Item Name",
|
||||||
|
"price": 10.50
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"trade": {
|
||||||
|
"tradeId": "db_id",
|
||||||
|
"offerId": "steam_id",
|
||||||
|
"verificationCode": "A3X9K2",
|
||||||
|
"itemCount": 1,
|
||||||
|
"totalValue": 10.50,
|
||||||
|
"status": "pending"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### GET /api/inventory/trades
|
||||||
|
|
||||||
|
Get user's trade history.
|
||||||
|
|
||||||
|
**Response**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"trades": [
|
||||||
|
{
|
||||||
|
"_id": "507f1f77bcf86cd799439011",
|
||||||
|
"offerId": "5847362918",
|
||||||
|
"status": "completed",
|
||||||
|
"totalValue": 75.50,
|
||||||
|
"createdAt": "2024-01-01T00:00:00Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### GET /api/inventory/trade/:tradeId
|
||||||
|
|
||||||
|
Get specific trade details.
|
||||||
|
|
||||||
|
### POST /api/inventory/trade/:tradeId/cancel
|
||||||
|
|
||||||
|
Cancel a pending trade offer.
|
||||||
|
|
||||||
|
## Bot Configuration
|
||||||
|
|
||||||
|
### Config File Format
|
||||||
|
|
||||||
|
**File**: `config/steam-bots.json`
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"username": "turbotrades_bot1",
|
||||||
|
"password": "your_password",
|
||||||
|
"sharedSecret": "your_shared_secret",
|
||||||
|
"identitySecret": "your_identity_secret",
|
||||||
|
"steamApiKey": "your_steam_api_key",
|
||||||
|
"proxy": {
|
||||||
|
"type": "socks5",
|
||||||
|
"host": "proxy.example.com",
|
||||||
|
"port": 1080,
|
||||||
|
"username": "proxy_user",
|
||||||
|
"password": "proxy_pass"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Auto-start bots on server startup
|
||||||
|
STEAM_BOT_AUTO_START=true
|
||||||
|
|
||||||
|
# Steam API key for inventory fetching
|
||||||
|
STEAM_APIS_KEY=your_api_key
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Trade Creation Errors
|
||||||
|
|
||||||
|
| Error | Cause | Solution |
|
||||||
|
|-------|-------|----------|
|
||||||
|
| `No bots available` | All bots offline/busy | Wait or add more bots |
|
||||||
|
| `Trade URL is required` | User hasn't set trade URL | Redirect to profile settings |
|
||||||
|
| `Bot not ready` | Bot not logged in | Check bot status, restart if needed |
|
||||||
|
| `Invalid trade URL` | Malformed URL | User must update trade URL |
|
||||||
|
|
||||||
|
### Trade State Errors
|
||||||
|
|
||||||
|
- **Declined**: User declined offer - notify user, can retry
|
||||||
|
- **Expired**: 15 days passed - notify user, items returned to user
|
||||||
|
- **Canceled**: Bot/user canceled - update status, no funds credited
|
||||||
|
- **Invalid**: Steam error - log error, notify admin
|
||||||
|
|
||||||
|
## Monitoring & Logging
|
||||||
|
|
||||||
|
### Bot Health Checks
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
botManager.getAllBotsHealth();
|
||||||
|
// Returns health status for all bots
|
||||||
|
```
|
||||||
|
|
||||||
|
### Trade Statistics
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
botManager.getStats();
|
||||||
|
// Returns total trades, active trades, errors, etc.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Indexes
|
||||||
|
|
||||||
|
- `Trade.offerId` - Fast lookup by Steam offer ID
|
||||||
|
- `Trade.user` + `Trade.status` - User's pending trades
|
||||||
|
- `Transaction.user` + `Transaction.createdAt` - Transaction history
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Test Sell Flow
|
||||||
|
|
||||||
|
1. Set up test bot with Steam account
|
||||||
|
2. Configure bot credentials in `config/steam-bots.json`
|
||||||
|
3. Start backend with `STEAM_BOT_AUTO_START=true`
|
||||||
|
4. Set trade URL in profile
|
||||||
|
5. Navigate to `/sell` page
|
||||||
|
6. Select items and click sell
|
||||||
|
7. Check verification code matches
|
||||||
|
8. Accept trade in Steam
|
||||||
|
9. Verify balance credited
|
||||||
|
|
||||||
|
### Mock Testing (No Real Trades)
|
||||||
|
|
||||||
|
For development without real Steam bots:
|
||||||
|
- Comment out bot initialization
|
||||||
|
- Create mock trade records directly
|
||||||
|
- Test WebSocket notifications
|
||||||
|
- Test UI with fake verification codes
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Trade Not Creating
|
||||||
|
|
||||||
|
1. Check bot status: `GET /api/admin/bots/health`
|
||||||
|
2. Verify Steam API key is valid
|
||||||
|
3. Check bot login credentials
|
||||||
|
4. Review bot error logs
|
||||||
|
5. Ensure identity_secret is correct
|
||||||
|
|
||||||
|
### Trade Created But Not Accepted
|
||||||
|
|
||||||
|
1. Verify user has mobile authenticator
|
||||||
|
2. Check if items are still tradable
|
||||||
|
3. Verify verification codes match
|
||||||
|
4. Check Steam trade restrictions (7-day holds)
|
||||||
|
|
||||||
|
### Balance Not Credited
|
||||||
|
|
||||||
|
1. Check trade status in database
|
||||||
|
2. Review server logs for `tradeAccepted` event
|
||||||
|
3. Verify transaction was created
|
||||||
|
4. Check for any database errors
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
- [ ] Queue system (Bull/Redis) for high-volume trades
|
||||||
|
- [ ] Trade status page with live updates
|
||||||
|
- [ ] Mobile notifications when trade is ready
|
||||||
|
- [ ] Automatic retry on trade failure
|
||||||
|
- [ ] Multi-region bot distribution
|
||||||
|
- [ ] Trade escrow handling for new devices
|
||||||
|
- [ ] Price history tracking per trade
|
||||||
|
- [ ] CSV export of trade history
|
||||||
|
- [ ] Admin dashboard for trade monitoring
|
||||||
34
config/steam-bots.example.json
Normal file
34
config/steam-bots.example.json
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"username": "turbotrades_bot1",
|
||||||
|
"password": "your_steam_password_here",
|
||||||
|
"sharedSecret": "your_shared_secret_here",
|
||||||
|
"identitySecret": "your_identity_secret_here",
|
||||||
|
"steamApiKey": "your_steam_api_key_here",
|
||||||
|
"pollInterval": 30000,
|
||||||
|
"tradeTimeout": 600000,
|
||||||
|
"proxy": {
|
||||||
|
"type": "socks5",
|
||||||
|
"host": "proxy.example.com",
|
||||||
|
"port": 1080,
|
||||||
|
"username": "proxy_user",
|
||||||
|
"password": "proxy_password"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "turbotrades_bot2",
|
||||||
|
"password": "your_steam_password_here",
|
||||||
|
"sharedSecret": "your_shared_secret_here",
|
||||||
|
"identitySecret": "your_identity_secret_here",
|
||||||
|
"steamApiKey": "your_steam_api_key_here",
|
||||||
|
"pollInterval": 30000,
|
||||||
|
"tradeTimeout": 600000,
|
||||||
|
"proxy": {
|
||||||
|
"type": "http",
|
||||||
|
"host": "proxy2.example.com",
|
||||||
|
"port": 8080,
|
||||||
|
"username": "proxy_user",
|
||||||
|
"password": "proxy_password"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -319,28 +319,37 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Confirm Sale Modal -->
|
<!-- Trade Status Modal -->
|
||||||
<div
|
<div
|
||||||
v-if="showConfirmModal"
|
v-if="showTradeModal"
|
||||||
class="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4"
|
class="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4"
|
||||||
@click.self="showConfirmModal = false"
|
@click.self="
|
||||||
|
tradeState === 'created' || tradeState === 'error'
|
||||||
|
? null
|
||||||
|
: (showTradeModal = false)
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="bg-surface-light rounded-lg max-w-md w-full p-6 border border-surface-lighter"
|
class="bg-surface-light rounded-lg max-w-md w-full p-6 border border-surface-lighter"
|
||||||
>
|
>
|
||||||
<!-- Modal Header -->
|
<!-- Modal Header -->
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<h3 class="text-xl font-bold text-white">Confirm Sale</h3>
|
<h3 class="text-xl font-bold text-white">{{ tradeModalTitle }}</h3>
|
||||||
<button
|
<button
|
||||||
@click="showConfirmModal = false"
|
v-if="tradeState !== 'confirming' && tradeState !== 'created'"
|
||||||
|
@click="
|
||||||
|
showTradeModal = false;
|
||||||
|
currentTrade = null;
|
||||||
|
selectedItems = [];
|
||||||
|
"
|
||||||
class="text-text-secondary hover:text-white transition-colors"
|
class="text-text-secondary hover:text-white transition-colors"
|
||||||
>
|
>
|
||||||
<X class="w-6 h-6" />
|
<X class="w-6 h-6" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Modal Content -->
|
<!-- Confirming State -->
|
||||||
<div class="space-y-4 mb-6">
|
<div v-if="tradeState === 'confirming'" class="space-y-4 mb-6">
|
||||||
<p class="text-text-secondary">
|
<p class="text-text-secondary">
|
||||||
You're about to sell
|
You're about to sell
|
||||||
<strong class="text-white">{{ selectedItems.length }}</strong>
|
<strong class="text-white">{{ selectedItems.length }}</strong>
|
||||||
@@ -375,28 +384,195 @@
|
|||||||
<AlertCircle class="w-5 h-5 text-primary flex-shrink-0 mt-0.5" />
|
<AlertCircle class="w-5 h-5 text-primary flex-shrink-0 mt-0.5" />
|
||||||
<p class="text-sm text-text-secondary">
|
<p class="text-sm text-text-secondary">
|
||||||
<strong class="text-white">Important:</strong> You will receive a
|
<strong class="text-white">Important:</strong> You will receive a
|
||||||
Steam trade offer shortly. Please accept it to complete the sale.
|
Steam trade offer. Please verify the code before accepting.
|
||||||
Funds will be credited to your balance after the trade is
|
|
||||||
accepted.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Actions -->
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<button
|
||||||
|
@click="showTradeModal = false"
|
||||||
|
class="flex-1 px-4 py-2.5 bg-surface hover:bg-surface-lighter text-white rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="confirmSale"
|
||||||
|
:disabled="isProcessing"
|
||||||
|
class="flex-1 px-4 py-2.5 bg-gradient-to-r from-primary to-primary-dark text-surface-dark font-semibold rounded-lg hover:opacity-90 transition-opacity disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<Loader2 v-if="isProcessing" class="w-4 h-4 animate-spin" />
|
||||||
|
<span>{{ isProcessing ? "Processing..." : "Confirm Sale" }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Modal Actions -->
|
<!-- Trade Created State -->
|
||||||
<div class="flex items-center gap-3">
|
<div v-else-if="tradeState === 'created'" class="space-y-4">
|
||||||
<button
|
<div class="text-center py-4">
|
||||||
@click="showConfirmModal = false"
|
<div
|
||||||
class="flex-1 px-4 py-2.5 bg-surface hover:bg-surface-lighter text-white rounded-lg transition-colors"
|
class="w-16 h-16 bg-primary/20 rounded-full flex items-center justify-center mx-auto mb-4"
|
||||||
|
>
|
||||||
|
<CheckCircle class="w-8 h-8 text-primary" />
|
||||||
|
</div>
|
||||||
|
<p class="text-white font-semibold mb-2">Trade Offer Created!</p>
|
||||||
|
<p class="text-text-secondary text-sm">
|
||||||
|
Check your Steam for the trade offer
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Verification Code Display -->
|
||||||
|
<div
|
||||||
|
class="bg-gradient-to-br from-primary/20 to-primary-dark/20 border-2 border-primary rounded-lg p-6 text-center"
|
||||||
>
|
>
|
||||||
Cancel
|
<p class="text-text-secondary text-sm mb-2">Verification Code</p>
|
||||||
|
<p class="text-4xl font-bold text-white tracking-widest font-mono">
|
||||||
|
{{ currentTrade?.verificationCode }}
|
||||||
|
</p>
|
||||||
|
<p class="text-text-secondary text-xs mt-2">
|
||||||
|
Match this code with the one in your Steam trade offer
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Open Trade Link Button -->
|
||||||
|
<a
|
||||||
|
v-if="currentTrade?.tradeOfferUrl"
|
||||||
|
:href="currentTrade.tradeOfferUrl"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="w-full px-6 py-3 bg-gradient-to-r from-primary to-primary-dark text-surface-dark font-semibold rounded-lg hover:opacity-90 transition-opacity flex items-center justify-center gap-2 text-center"
|
||||||
|
>
|
||||||
|
<ExternalLink class="w-5 h-5" />
|
||||||
|
Open Trade in Steam
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Trade Details -->
|
||||||
|
<div class="bg-surface rounded-lg p-4 space-y-2">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-text-secondary">Items:</span>
|
||||||
|
<span class="text-white font-semibold">
|
||||||
|
{{ currentTrade?.itemCount }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-text-secondary">Value:</span>
|
||||||
|
<span class="text-white font-semibold">
|
||||||
|
{{ formatCurrency(currentTrade?.totalValue || 0) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-text-secondary">Status:</span>
|
||||||
|
<span
|
||||||
|
class="text-yellow-400 font-semibold flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<Loader2 class="w-3 h-3 animate-spin" />
|
||||||
|
Pending
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Instructions -->
|
||||||
|
<div
|
||||||
|
class="bg-primary/10 border border-primary/30 rounded-lg p-3 space-y-2"
|
||||||
|
>
|
||||||
|
<p class="text-white font-semibold text-sm">Next Steps:</p>
|
||||||
|
<ol
|
||||||
|
class="text-text-secondary text-sm space-y-1 list-decimal list-inside"
|
||||||
|
>
|
||||||
|
<li>Click "Open Trade in Steam" button above</li>
|
||||||
|
<li>
|
||||||
|
Verify the code matches:
|
||||||
|
<span class="text-primary font-mono font-bold">{{
|
||||||
|
currentTrade?.verificationCode
|
||||||
|
}}</span>
|
||||||
|
</li>
|
||||||
|
<li>Accept the trade</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="
|
||||||
|
showTradeModal = false;
|
||||||
|
currentTrade = null;
|
||||||
|
selectedItems = [];
|
||||||
|
"
|
||||||
|
class="w-full px-4 py-2.5 bg-surface hover:bg-surface-lighter text-white rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
Close
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Trade Accepted State -->
|
||||||
|
<div v-else-if="tradeState === 'accepted'" class="space-y-4">
|
||||||
|
<div class="text-center py-4">
|
||||||
|
<div
|
||||||
|
class="w-16 h-16 bg-green-500/20 rounded-full flex items-center justify-center mx-auto mb-4"
|
||||||
|
>
|
||||||
|
<CheckCircle class="w-8 h-8 text-green-500" />
|
||||||
|
</div>
|
||||||
|
<p class="text-white font-semibold text-lg mb-2">Trade Complete!</p>
|
||||||
|
<p class="text-text-secondary text-sm">
|
||||||
|
Your balance has been credited
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-surface rounded-lg p-4 space-y-2">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-text-secondary">Amount Credited:</span>
|
||||||
|
<span class="text-green-500 font-bold text-xl">
|
||||||
|
+{{
|
||||||
|
formatCurrency(
|
||||||
|
currentTrade?.amount || currentTrade?.totalValue || 0
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-text-secondary">New Balance:</span>
|
||||||
|
<span class="text-white font-semibold">
|
||||||
|
{{
|
||||||
|
formatCurrency(
|
||||||
|
currentTrade?.newBalance || authStore.user?.balance || 0
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@click="confirmSale"
|
@click="
|
||||||
:disabled="isProcessing"
|
showTradeModal = false;
|
||||||
class="flex-1 px-4 py-2.5 bg-gradient-to-r from-primary to-primary-dark text-surface-dark font-semibold rounded-lg hover:opacity-90 transition-opacity disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
currentTrade = null;
|
||||||
|
selectedItems = [];
|
||||||
|
"
|
||||||
|
class="w-full px-4 py-2.5 bg-gradient-to-r from-primary to-primary-dark text-surface-dark font-semibold rounded-lg hover:opacity-90 transition-opacity"
|
||||||
>
|
>
|
||||||
<Loader2 v-if="isProcessing" class="w-4 h-4 animate-spin" />
|
Done
|
||||||
<span>{{ isProcessing ? "Processing..." : "Confirm Sale" }}</span>
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error State -->
|
||||||
|
<div v-else-if="tradeState === 'error'" class="space-y-4">
|
||||||
|
<div class="text-center py-4">
|
||||||
|
<div
|
||||||
|
class="w-16 h-16 bg-red-500/20 rounded-full flex items-center justify-center mx-auto mb-4"
|
||||||
|
>
|
||||||
|
<AlertCircle class="w-8 h-8 text-red-500" />
|
||||||
|
</div>
|
||||||
|
<p class="text-white font-semibold text-lg mb-2">Trade Failed</p>
|
||||||
|
<p class="text-text-secondary text-sm">{{ tradeError }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="
|
||||||
|
showTradeModal = false;
|
||||||
|
currentTrade = null;
|
||||||
|
tradeState = 'confirming';
|
||||||
|
tradeError = null;
|
||||||
|
"
|
||||||
|
class="w-full px-4 py-2.5 bg-surface hover:bg-surface-lighter text-white rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
Close
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -405,7 +581,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from "vue";
|
import { ref, computed, onMounted, onUnmounted } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
import axios from "@/utils/axios";
|
import axios from "@/utils/axios";
|
||||||
@@ -424,6 +600,7 @@ import {
|
|||||||
Settings,
|
Settings,
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
|
ExternalLink,
|
||||||
} from "lucide-vue-next";
|
} from "lucide-vue-next";
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -437,7 +614,10 @@ const selectedItems = ref([]);
|
|||||||
const isLoading = ref(false);
|
const isLoading = ref(false);
|
||||||
const isProcessing = ref(false);
|
const isProcessing = ref(false);
|
||||||
|
|
||||||
const showConfirmModal = ref(false);
|
const showTradeModal = ref(false);
|
||||||
|
const tradeState = ref("confirming"); // confirming, created, accepted, error
|
||||||
|
const currentTrade = ref(null);
|
||||||
|
const tradeError = ref(null);
|
||||||
const searchQuery = ref("");
|
const searchQuery = ref("");
|
||||||
const selectedGame = ref("cs2");
|
const selectedGame = ref("cs2");
|
||||||
const sortBy = ref("price-desc");
|
const sortBy = ref("price-desc");
|
||||||
@@ -463,6 +643,24 @@ const totalSelectedValue = computed(() => {
|
|||||||
}, 0);
|
}, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const tradeModalTitle = computed(() => {
|
||||||
|
switch (tradeState.value) {
|
||||||
|
case "confirming":
|
||||||
|
return "Confirm Sale";
|
||||||
|
case "created":
|
||||||
|
return "Trade Offer Created";
|
||||||
|
case "accepted":
|
||||||
|
return "Trade Complete!";
|
||||||
|
case "error":
|
||||||
|
return "Trade Failed";
|
||||||
|
default:
|
||||||
|
return "Trade Status";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// WebSocket connection for real-time updates
|
||||||
|
let wsMessageHandler = null;
|
||||||
|
|
||||||
// Methods
|
// Methods
|
||||||
const fetchInventory = async () => {
|
const fetchInventory = async () => {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
@@ -596,7 +794,11 @@ const handleSellClick = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
showConfirmModal.value = true;
|
// Reset modal state
|
||||||
|
tradeState.value = "confirming";
|
||||||
|
currentTrade.value = null;
|
||||||
|
tradeError.value = null;
|
||||||
|
showTradeModal.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmSale = async () => {
|
const confirmSale = async () => {
|
||||||
@@ -604,7 +806,7 @@ const confirmSale = async () => {
|
|||||||
|
|
||||||
if (!hasTradeUrl.value) {
|
if (!hasTradeUrl.value) {
|
||||||
toast.error("Trade URL is required to sell items");
|
toast.error("Trade URL is required to sell items");
|
||||||
showConfirmModal.value = false;
|
showTradeModal.value = false;
|
||||||
router.push("/profile");
|
router.push("/profile");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -613,8 +815,11 @@ const confirmSale = async () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.post("/api/inventory/sell", {
|
const response = await axios.post("/api/inventory/sell", {
|
||||||
|
tradeUrl: authStore.user?.tradeUrl,
|
||||||
items: selectedItems.value.map((item) => ({
|
items: selectedItems.value.map((item) => ({
|
||||||
assetid: item.assetid,
|
assetid: item.assetid,
|
||||||
|
appid: item.appid || (selectedGame.value === "cs2" ? 730 : 252490),
|
||||||
|
contextid: item.contextid || "2",
|
||||||
name: item.name,
|
name: item.name,
|
||||||
price: item.estimatedPrice,
|
price: item.estimatedPrice,
|
||||||
image: item.image,
|
image: item.image,
|
||||||
@@ -627,20 +832,11 @@ const confirmSale = async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (response.data.success) {
|
if (response.data.success) {
|
||||||
toast.success(
|
const trade = response.data.trade;
|
||||||
`Successfully listed ${selectedItems.value.length} item${
|
|
||||||
selectedItems.value.length > 1 ? "s" : ""
|
|
||||||
} for ${formatCurrency(response.data.totalEarned)}!`
|
|
||||||
);
|
|
||||||
|
|
||||||
toast.info(
|
// Update modal state to show verification code
|
||||||
"You will receive a Steam trade offer shortly. Please accept it to complete the sale."
|
currentTrade.value = trade;
|
||||||
);
|
tradeState.value = "created";
|
||||||
|
|
||||||
// Update balance
|
|
||||||
if (response.data.newBalance !== undefined) {
|
|
||||||
authStore.updateBalance(response.data.newBalance);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove sold items from list
|
// Remove sold items from list
|
||||||
const soldAssetIds = selectedItems.value.map((item) => item.assetid);
|
const soldAssetIds = selectedItems.value.map((item) => item.assetid);
|
||||||
@@ -650,22 +846,82 @@ const confirmSale = async () => {
|
|||||||
filteredItems.value = filteredItems.value.filter(
|
filteredItems.value = filteredItems.value.filter(
|
||||||
(item) => !soldAssetIds.includes(item.assetid)
|
(item) => !soldAssetIds.includes(item.assetid)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Clear selection and close modal
|
|
||||||
selectedItems.value = [];
|
|
||||||
showConfirmModal.value = false;
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to sell items:", err);
|
console.error("Failed to create trade:", err);
|
||||||
const message =
|
const message =
|
||||||
err.response?.data?.message ||
|
err.response?.data?.message ||
|
||||||
"Failed to complete sale. Please try again.";
|
"Failed to create trade offer. Please try again.";
|
||||||
toast.error(message);
|
|
||||||
|
tradeError.value = message;
|
||||||
|
tradeState.value = "error";
|
||||||
} finally {
|
} finally {
|
||||||
isProcessing.value = false;
|
isProcessing.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Listen for WebSocket trade updates
|
||||||
|
const setupWebSocketListeners = () => {
|
||||||
|
// Get WebSocket from auth store if available
|
||||||
|
if (authStore.ws) {
|
||||||
|
wsMessageHandler = (event) => {
|
||||||
|
try {
|
||||||
|
const message = JSON.parse(event.data);
|
||||||
|
|
||||||
|
if (message.type === "trade_completed" && currentTrade.value) {
|
||||||
|
// Trade was accepted and completed
|
||||||
|
if (message.data.tradeId === currentTrade.value.tradeId) {
|
||||||
|
currentTrade.value = {
|
||||||
|
...currentTrade.value,
|
||||||
|
...message.data,
|
||||||
|
};
|
||||||
|
tradeState.value = "accepted";
|
||||||
|
|
||||||
|
// Update balance
|
||||||
|
if (message.data.newBalance !== undefined) {
|
||||||
|
authStore.updateBalance(message.data.newBalance);
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success(
|
||||||
|
`Trade completed! +${formatCurrency(message.data.amount)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (message.type === "trade_declined" && currentTrade.value) {
|
||||||
|
if (message.data.tradeId === currentTrade.value.tradeId) {
|
||||||
|
tradeError.value = "Trade was declined on Steam";
|
||||||
|
tradeState.value = "error";
|
||||||
|
}
|
||||||
|
} else if (message.type === "trade_expired" && currentTrade.value) {
|
||||||
|
if (message.data.tradeId === currentTrade.value.tradeId) {
|
||||||
|
tradeError.value = "Trade offer expired";
|
||||||
|
tradeState.value = "error";
|
||||||
|
}
|
||||||
|
} else if (message.type === "balance_update") {
|
||||||
|
authStore.updateBalance(message.data.balance);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error handling WebSocket message:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
authStore.ws.addEventListener("message", wsMessageHandler);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleanupWebSocketListeners = () => {
|
||||||
|
if (authStore.ws && wsMessageHandler) {
|
||||||
|
authStore.ws.removeEventListener("message", wsMessageHandler);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
setupWebSocketListeners();
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
cleanupWebSocketListeners();
|
||||||
|
});
|
||||||
|
|
||||||
const formatCurrency = (amount) => {
|
const formatCurrency = (amount) => {
|
||||||
return new Intl.NumberFormat("en-US", {
|
return new Intl.NumberFormat("en-US", {
|
||||||
style: "currency",
|
style: "currency",
|
||||||
|
|||||||
@@ -9,11 +9,67 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Pending Trades Section -->
|
||||||
|
<div v-if="pendingTrades.length > 0" class="mb-6">
|
||||||
|
<h2 class="text-xl font-bold text-white mb-4 flex items-center gap-2">
|
||||||
|
<Clock class="w-5 h-5 text-yellow-400" />
|
||||||
|
Pending Trades
|
||||||
|
</h2>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div
|
||||||
|
v-for="trade in pendingTrades"
|
||||||
|
:key="trade._id"
|
||||||
|
class="bg-surface-light rounded-lg border border-yellow-400/30 p-4"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between flex-wrap gap-4">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div
|
||||||
|
class="w-2 h-2 bg-yellow-400 rounded-full animate-pulse"
|
||||||
|
></div>
|
||||||
|
<span class="text-white font-medium">
|
||||||
|
Selling {{ trade.items?.length || 0 }} item(s)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="h-6 w-px bg-surface-lighter"></div>
|
||||||
|
<div class="text-text-secondary text-sm">
|
||||||
|
Code:
|
||||||
|
<span class="text-primary font-mono font-bold">{{
|
||||||
|
trade.verificationCode
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="text-white font-semibold">
|
||||||
|
{{ formatCurrency(trade.totalValue || 0) }}
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
v-if="trade.tradeOfferUrl"
|
||||||
|
:href="trade.tradeOfferUrl"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="px-4 py-2 bg-primary hover:bg-primary-dark text-surface-dark font-medium rounded-lg transition-colors text-sm flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<ExternalLink class="w-4 h-4" />
|
||||||
|
Open
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
@click="viewTradeDetails(trade)"
|
||||||
|
class="px-4 py-2 bg-primary/20 hover:bg-primary/30 text-primary rounded-lg transition-colors text-sm"
|
||||||
|
>
|
||||||
|
Details
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Filters -->
|
<!-- Filters -->
|
||||||
<div
|
<div
|
||||||
class="bg-surface-light rounded-lg border border-surface-lighter p-6 mb-6"
|
class="bg-surface-light rounded-lg border border-surface-lighter p-6 mb-6"
|
||||||
>
|
>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-text-secondary mb-2">
|
<label class="block text-sm font-medium text-text-secondary mb-2">
|
||||||
Type
|
Type
|
||||||
@@ -65,12 +121,6 @@
|
|||||||
<option value="year">Last Year</option>
|
<option value="year">Last Year</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-end">
|
|
||||||
<button @click="resetFilters" class="btn-secondary w-full">
|
|
||||||
Reset Filters
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -417,11 +467,112 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Trade Details Modal -->
|
||||||
|
<div
|
||||||
|
v-if="showTradeModal && selectedTrade"
|
||||||
|
class="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4"
|
||||||
|
@click.self="closeTradeModal"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="bg-surface-light rounded-lg max-w-md w-full p-6 border border-surface-lighter"
|
||||||
|
>
|
||||||
|
<!-- Modal Header -->
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h3 class="text-xl font-bold text-white">Trade Details</h3>
|
||||||
|
<button
|
||||||
|
@click="closeTradeModal"
|
||||||
|
class="text-text-secondary hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
<X class="w-6 h-6" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Trade Status -->
|
||||||
|
<div class="text-center py-4 mb-4">
|
||||||
|
<div
|
||||||
|
class="w-16 h-16 bg-yellow-400/20 rounded-full flex items-center justify-center mx-auto mb-3"
|
||||||
|
>
|
||||||
|
<Clock class="w-8 h-8 text-yellow-400" />
|
||||||
|
</div>
|
||||||
|
<p class="text-white font-semibold mb-1">Waiting for Acceptance</p>
|
||||||
|
<p class="text-text-secondary text-sm">
|
||||||
|
Check your Steam trade offers
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Verification Code -->
|
||||||
|
<div
|
||||||
|
class="bg-gradient-to-br from-primary/20 to-primary-dark/20 border-2 border-primary rounded-lg p-6 text-center mb-4"
|
||||||
|
>
|
||||||
|
<p class="text-text-secondary text-sm mb-2">Verification Code</p>
|
||||||
|
<p class="text-4xl font-bold text-white tracking-widest font-mono">
|
||||||
|
{{ selectedTrade.verificationCode }}
|
||||||
|
</p>
|
||||||
|
<p class="text-text-secondary text-xs mt-2">
|
||||||
|
Match this code with your Steam trade offer
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Open Trade Link Button -->
|
||||||
|
<a
|
||||||
|
v-if="selectedTrade.tradeOfferUrl"
|
||||||
|
:href="selectedTrade.tradeOfferUrl"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="w-full px-6 py-3 bg-gradient-to-r from-primary to-primary-dark text-surface-dark font-semibold rounded-lg hover:opacity-90 transition-opacity flex items-center justify-center gap-2 text-center mb-4"
|
||||||
|
>
|
||||||
|
<ExternalLink class="w-5 h-5" />
|
||||||
|
Open Trade in Steam
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Trade Info -->
|
||||||
|
<div class="bg-surface rounded-lg p-4 space-y-2 mb-4">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-text-secondary">Items:</span>
|
||||||
|
<span class="text-white font-semibold">{{
|
||||||
|
selectedTrade.items?.length || 0
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-text-secondary">Value:</span>
|
||||||
|
<span class="text-white font-semibold">{{
|
||||||
|
formatCurrency(selectedTrade.totalValue || 0)
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-text-secondary">Created:</span>
|
||||||
|
<span class="text-white font-semibold">{{
|
||||||
|
formatDate(selectedTrade.sentAt || selectedTrade.createdAt)
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Instructions -->
|
||||||
|
<div class="bg-primary/10 border border-primary/30 rounded-lg p-3 mb-4">
|
||||||
|
<p class="text-white font-semibold text-sm mb-2">Instructions:</p>
|
||||||
|
<ol
|
||||||
|
class="text-text-secondary text-sm space-y-1 list-decimal list-inside"
|
||||||
|
>
|
||||||
|
<li>Click "Open Trade in Steam" button above</li>
|
||||||
|
<li>Verify the code matches</li>
|
||||||
|
<li>Accept the trade</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="closeTradeModal"
|
||||||
|
class="w-full px-4 py-2.5 bg-surface hover:bg-surface-lighter text-white rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, watch } from "vue";
|
import { ref, computed, onMounted, onUnmounted, watch } from "vue";
|
||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
import axios from "@/utils/axios";
|
import axios from "@/utils/axios";
|
||||||
import { useToast } from "vue-toastification";
|
import { useToast } from "vue-toastification";
|
||||||
@@ -443,6 +594,9 @@ import {
|
|||||||
RefreshCw,
|
RefreshCw,
|
||||||
Gift,
|
Gift,
|
||||||
DollarSign,
|
DollarSign,
|
||||||
|
Clock,
|
||||||
|
X,
|
||||||
|
ExternalLink,
|
||||||
} from "lucide-vue-next";
|
} from "lucide-vue-next";
|
||||||
|
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
@@ -455,6 +609,9 @@ const currentPage = ref(1);
|
|||||||
const perPage = ref(10);
|
const perPage = ref(10);
|
||||||
const totalTransactions = ref(0);
|
const totalTransactions = ref(0);
|
||||||
const expandedTransaction = ref(null);
|
const expandedTransaction = ref(null);
|
||||||
|
const pendingTrades = ref([]);
|
||||||
|
const showTradeModal = ref(false);
|
||||||
|
const selectedTrade = ref(null);
|
||||||
|
|
||||||
const filters = ref({
|
const filters = ref({
|
||||||
type: "",
|
type: "",
|
||||||
@@ -462,6 +619,8 @@ const filters = ref({
|
|||||||
dateRange: "all",
|
dateRange: "all",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let wsMessageHandler = null;
|
||||||
|
|
||||||
const stats = ref({
|
const stats = ref({
|
||||||
totalDeposits: 0,
|
totalDeposits: 0,
|
||||||
totalWithdrawals: 0,
|
totalWithdrawals: 0,
|
||||||
@@ -557,13 +716,83 @@ const fetchTransactions = async () => {
|
|||||||
console.error("Response:", error.response?.data);
|
console.error("Response:", error.response?.data);
|
||||||
console.error("Status:", error.response?.status);
|
console.error("Status:", error.response?.status);
|
||||||
if (error.response?.status !== 404) {
|
if (error.response?.status !== 404) {
|
||||||
toast.error("Failed to load transactions");
|
toast.error("Failed to load transaction history");
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchPendingTrades = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get("/api/inventory/trades");
|
||||||
|
if (response.data.success) {
|
||||||
|
pendingTrades.value = response.data.trades.filter(
|
||||||
|
(t) => t.state === "pending"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to load pending trades:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const viewTradeDetails = (trade) => {
|
||||||
|
selectedTrade.value = trade;
|
||||||
|
showTradeModal.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeTradeModal = () => {
|
||||||
|
showTradeModal.value = false;
|
||||||
|
selectedTrade.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setupWebSocketListeners = () => {
|
||||||
|
if (authStore.ws) {
|
||||||
|
wsMessageHandler = (event) => {
|
||||||
|
try {
|
||||||
|
const message = JSON.parse(event.data);
|
||||||
|
|
||||||
|
if (message.type === "trade_completed") {
|
||||||
|
// Remove from pending
|
||||||
|
pendingTrades.value = pendingTrades.value.filter(
|
||||||
|
(t) => t._id !== message.data.tradeId
|
||||||
|
);
|
||||||
|
// Refresh transactions
|
||||||
|
fetchTransactions();
|
||||||
|
toast.success("Trade completed! Balance updated.");
|
||||||
|
} else if (
|
||||||
|
message.type === "trade_declined" ||
|
||||||
|
message.type === "trade_expired" ||
|
||||||
|
message.type === "trade_canceled"
|
||||||
|
) {
|
||||||
|
// Remove from pending
|
||||||
|
pendingTrades.value = pendingTrades.value.filter(
|
||||||
|
(t) => t._id !== message.data.tradeId
|
||||||
|
);
|
||||||
|
if (message.type === "trade_declined") {
|
||||||
|
toast.warning("Trade was declined");
|
||||||
|
} else if (message.type === "trade_expired") {
|
||||||
|
toast.warning("Trade offer expired");
|
||||||
|
}
|
||||||
|
} else if (message.type === "trade_created") {
|
||||||
|
// Add to pending trades
|
||||||
|
fetchPendingTrades();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error handling WebSocket message:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
authStore.ws.addEventListener("message", wsMessageHandler);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleanupWebSocketListeners = () => {
|
||||||
|
if (authStore.ws && wsMessageHandler) {
|
||||||
|
authStore.ws.removeEventListener("message", wsMessageHandler);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const nextPage = () => {
|
const nextPage = () => {
|
||||||
if (hasNextPage.value) {
|
if (hasNextPage.value) {
|
||||||
currentPage.value++;
|
currentPage.value++;
|
||||||
@@ -731,5 +960,11 @@ const getSessionColor = (sessionIdShort) => {
|
|||||||
// Lifecycle
|
// Lifecycle
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchTransactions();
|
fetchTransactions();
|
||||||
|
fetchPendingTrades();
|
||||||
|
setupWebSocketListeners();
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
cleanupWebSocketListeners();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
192
index.js
192
index.js
@@ -20,6 +20,12 @@ import adminRoutes from "./routes/admin.js";
|
|||||||
|
|
||||||
// Import services
|
// Import services
|
||||||
import pricingService from "./services/pricing.js";
|
import pricingService from "./services/pricing.js";
|
||||||
|
import { getSteamBotManager } from "./services/steamBot.js";
|
||||||
|
|
||||||
|
// Import models
|
||||||
|
import User from "./models/User.js";
|
||||||
|
import Trade from "./models/Trade.js";
|
||||||
|
import Transaction from "./models/Transaction.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create and configure Fastify server
|
* Create and configure Fastify server
|
||||||
@@ -342,6 +348,192 @@ const start = async () => {
|
|||||||
// Add WebSocket manager to fastify instance
|
// Add WebSocket manager to fastify instance
|
||||||
fastify.decorate("websocketManager", wsManager);
|
fastify.decorate("websocketManager", wsManager);
|
||||||
|
|
||||||
|
// Initialize Steam Bot Manager
|
||||||
|
const botManager = getSteamBotManager();
|
||||||
|
|
||||||
|
// Setup trade event listeners
|
||||||
|
botManager.on("tradeAccepted", async (offer, tradeData) => {
|
||||||
|
console.log(`✅ Trade ${offer.id} accepted! Crediting user...`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Find the trade record
|
||||||
|
const trade = await Trade.findOne({ offerId: offer.id });
|
||||||
|
|
||||||
|
if (!trade) {
|
||||||
|
console.error(`❌ Trade record not found for offer ${offer.id}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trade.state === "accepted") {
|
||||||
|
console.log(`⚠️ Trade ${offer.id} already completed, skipping`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the user
|
||||||
|
const user = await User.findById(trade.userId);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
console.error(`❌ User not found for trade ${offer.id}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Credit user balance
|
||||||
|
user.balance += trade.totalValue;
|
||||||
|
await user.save();
|
||||||
|
|
||||||
|
// Update trade status
|
||||||
|
trade.state = "accepted";
|
||||||
|
trade.completedAt = new Date();
|
||||||
|
await trade.save();
|
||||||
|
|
||||||
|
// Create transaction record
|
||||||
|
const transaction = new Transaction({
|
||||||
|
user: user._id,
|
||||||
|
type: "sale",
|
||||||
|
amount: trade.totalValue,
|
||||||
|
description: `Sold ${trade.items.length} item(s)`,
|
||||||
|
status: "completed",
|
||||||
|
metadata: {
|
||||||
|
tradeId: trade._id,
|
||||||
|
offerId: offer.id,
|
||||||
|
botId: tradeData.botId,
|
||||||
|
itemCount: trade.items.length,
|
||||||
|
verificationCode: trade.verificationCode,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await transaction.save();
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`✅ Credited $${trade.totalValue.toFixed(2)} to user ${
|
||||||
|
user.username
|
||||||
|
} (Balance: $${user.balance.toFixed(2)})`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Notify user via WebSocket
|
||||||
|
wsManager.sendToUser(user.steamId, {
|
||||||
|
type: "trade_completed",
|
||||||
|
data: {
|
||||||
|
tradeId: trade._id,
|
||||||
|
offerId: offer.id,
|
||||||
|
amount: trade.totalValue,
|
||||||
|
newBalance: user.balance,
|
||||||
|
itemCount: trade.items.length,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also send balance update
|
||||||
|
wsManager.sendToUser(user.steamId, {
|
||||||
|
type: "balance_update",
|
||||||
|
data: {
|
||||||
|
balance: user.balance,
|
||||||
|
change: trade.totalValue,
|
||||||
|
reason: "trade_completed",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ Error processing accepted trade ${offer.id}:`, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
botManager.on("tradeDeclined", async (offer, tradeData) => {
|
||||||
|
console.log(`❌ Trade ${offer.id} declined`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const trade = await Trade.findOne({ offerId: offer.id });
|
||||||
|
|
||||||
|
if (trade && trade.state === "pending") {
|
||||||
|
trade.state = "declined";
|
||||||
|
trade.completedAt = new Date();
|
||||||
|
await trade.save();
|
||||||
|
|
||||||
|
// Notify user
|
||||||
|
const user = await User.findById(trade.userId);
|
||||||
|
if (user) {
|
||||||
|
wsManager.sendToUser(user.steamId, {
|
||||||
|
type: "trade_declined",
|
||||||
|
data: {
|
||||||
|
tradeId: trade._id,
|
||||||
|
offerId: offer.id,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ Error processing declined trade ${offer.id}:`, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
botManager.on("tradeExpired", async (offer, tradeData) => {
|
||||||
|
console.log(`⏰ Trade ${offer.id} expired`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const trade = await Trade.findOne({ offerId: offer.id });
|
||||||
|
|
||||||
|
if (trade && trade.state === "pending") {
|
||||||
|
trade.state = "expired";
|
||||||
|
trade.completedAt = new Date();
|
||||||
|
await trade.save();
|
||||||
|
|
||||||
|
// Notify user
|
||||||
|
const user = await User.findById(trade.userId);
|
||||||
|
if (user) {
|
||||||
|
wsManager.sendToUser(user.steamId, {
|
||||||
|
type: "trade_expired",
|
||||||
|
data: {
|
||||||
|
tradeId: trade._id,
|
||||||
|
offerId: offer.id,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ Error processing expired trade ${offer.id}:`, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
botManager.on("tradeCanceled", async (offer, tradeData) => {
|
||||||
|
console.log(`🚫 Trade ${offer.id} canceled`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const trade = await Trade.findOne({ offerId: offer.id });
|
||||||
|
|
||||||
|
if (trade && trade.state === "pending") {
|
||||||
|
trade.state = "canceled";
|
||||||
|
trade.completedAt = new Date();
|
||||||
|
await trade.save();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ Error processing canceled trade ${offer.id}:`, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
botManager.on("botError", (error, botId) => {
|
||||||
|
console.error(`❌ Bot ${botId} error:`, error);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize bots if config exists
|
||||||
|
if (process.env.STEAM_BOT_AUTO_START === "true") {
|
||||||
|
try {
|
||||||
|
console.log("🤖 Auto-starting Steam bots...");
|
||||||
|
// You can load bot config from file or env vars
|
||||||
|
// const botsConfig = require("./config/steam-bots.json");
|
||||||
|
// await botManager.initialize(botsConfig);
|
||||||
|
console.log("⚠️ Bot auto-start enabled but no config found");
|
||||||
|
console.log(
|
||||||
|
" Configure bots in config/steam-bots.json or via env vars"
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Failed to initialize bots:", error.message);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
"🤖 Steam bots not auto-started (set STEAM_BOT_AUTO_START=true to enable)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Register plugins
|
// Register plugins
|
||||||
await registerPlugins(fastify);
|
await registerPlugins(fastify);
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { authenticate } from "../middleware/auth.js";
|
import { authenticate } from "../middleware/auth.js";
|
||||||
import Item from "../models/Item.js";
|
import Item from "../models/Item.js";
|
||||||
|
import Trade from "../models/Trade.js";
|
||||||
|
import Transaction from "../models/Transaction.js";
|
||||||
import { config } from "../config/index.js";
|
import { config } from "../config/index.js";
|
||||||
import pricingService from "../services/pricing.js";
|
import pricingService from "../services/pricing.js";
|
||||||
import marketPriceService from "../services/marketPrice.js";
|
import marketPriceService from "../services/marketPrice.js";
|
||||||
|
import { getSteamBotManager } from "../services/steamBot.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inventory routes for fetching and listing Steam items
|
* Inventory routes for fetching and listing Steam items
|
||||||
@@ -108,6 +111,8 @@ export default async function inventoryRoutes(fastify, options) {
|
|||||||
// Parse item details
|
// Parse item details
|
||||||
const item = {
|
const item = {
|
||||||
assetid: asset.assetid,
|
assetid: asset.assetid,
|
||||||
|
appid: appId,
|
||||||
|
contextid: contextId,
|
||||||
classid: asset.classid,
|
classid: asset.classid,
|
||||||
instanceid: asset.instanceid,
|
instanceid: asset.instanceid,
|
||||||
name: desc.market_hash_name || desc.name || "Unknown Item",
|
name: desc.market_hash_name || desc.name || "Unknown Item",
|
||||||
@@ -346,7 +351,7 @@ export default async function inventoryRoutes(fastify, options) {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// POST /inventory/sell - Sell items to the site
|
// POST /inventory/sell - Create Steam trade offer to sell items
|
||||||
fastify.post(
|
fastify.post(
|
||||||
"/sell",
|
"/sell",
|
||||||
{
|
{
|
||||||
@@ -354,15 +359,24 @@ export default async function inventoryRoutes(fastify, options) {
|
|||||||
schema: {
|
schema: {
|
||||||
body: {
|
body: {
|
||||||
type: "object",
|
type: "object",
|
||||||
required: ["items"],
|
required: ["items", "tradeUrl"],
|
||||||
properties: {
|
properties: {
|
||||||
items: {
|
items: {
|
||||||
type: "array",
|
type: "array",
|
||||||
items: {
|
items: {
|
||||||
type: "object",
|
type: "object",
|
||||||
required: ["assetid", "name", "price", "image"],
|
required: [
|
||||||
|
"assetid",
|
||||||
|
"appid",
|
||||||
|
"contextid",
|
||||||
|
"name",
|
||||||
|
"price",
|
||||||
|
"image",
|
||||||
|
],
|
||||||
properties: {
|
properties: {
|
||||||
assetid: { type: "string" },
|
assetid: { type: "string" },
|
||||||
|
appid: { type: "number" },
|
||||||
|
contextid: { type: "string" },
|
||||||
name: { type: "string" },
|
name: { type: "string" },
|
||||||
price: { type: "number" },
|
price: { type: "number" },
|
||||||
image: { type: "string" },
|
image: { type: "string" },
|
||||||
@@ -374,15 +388,29 @@ export default async function inventoryRoutes(fastify, options) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
tradeUrl: { type: "string" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async (request, reply) => {
|
async (request, reply) => {
|
||||||
|
// Declare variables outside try block for catch block access
|
||||||
|
let userId, steamId, items, tradeUrl, botManager, bypassBots;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { items } = request.body;
|
items = request.body.items;
|
||||||
const userId = request.user._id;
|
tradeUrl = request.body.tradeUrl;
|
||||||
const steamId = request.user.steamId;
|
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) {
|
if (!items || items.length === 0) {
|
||||||
return reply.status(400).send({
|
return reply.status(400).send({
|
||||||
@@ -391,103 +419,477 @@ export default async function inventoryRoutes(fastify, options) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
// Calculate total value
|
||||||
const totalValue = items.reduce((sum, item) => sum + item.price, 0);
|
const totalValue = items.reduce((sum, item) => sum + item.price, 0);
|
||||||
|
|
||||||
// Add items to marketplace
|
// Get bot manager
|
||||||
const createdItems = [];
|
botManager = getSteamBotManager();
|
||||||
for (const item of items) {
|
|
||||||
// Determine game based on item characteristics
|
|
||||||
const game = item.category?.includes("Rust") ? "rust" : "cs2";
|
|
||||||
|
|
||||||
// Map category
|
// In development mode, allow bypassing bot requirement
|
||||||
let categoryMapped = "other";
|
const isDevelopmentMode = process.env.NODE_ENV === "development";
|
||||||
if (item.category) {
|
bypassBots = process.env.BYPASS_BOT_REQUIREMENT === "true";
|
||||||
const cat = item.category.toLowerCase();
|
|
||||||
if (cat.includes("rifle")) categoryMapped = "rifles";
|
|
||||||
else if (cat.includes("pistol")) categoryMapped = "pistols";
|
|
||||||
else if (cat.includes("knife")) categoryMapped = "knives";
|
|
||||||
else if (cat.includes("glove")) categoryMapped = "gloves";
|
|
||||||
else if (cat.includes("smg")) categoryMapped = "smgs";
|
|
||||||
else if (cat.includes("sticker")) categoryMapped = "stickers";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map rarity
|
if (!botManager.isInitialized && !bypassBots) {
|
||||||
let rarityMapped = "common";
|
return reply.status(503).send({
|
||||||
if (item.rarity) {
|
success: false,
|
||||||
const rar = item.rarity.toLowerCase();
|
message:
|
||||||
if (rar.includes("contraband") || rar.includes("ancient"))
|
"Trade system is currently unavailable. Please try again later.",
|
||||||
rarityMapped = "exceedingly";
|
|
||||||
else if (rar.includes("covert") || rar.includes("legendary"))
|
|
||||||
rarityMapped = "legendary";
|
|
||||||
else if (rar.includes("classified") || rar.includes("mythical"))
|
|
||||||
rarityMapped = "mythical";
|
|
||||||
else if (rar.includes("restricted") || rar.includes("rare"))
|
|
||||||
rarityMapped = "rare";
|
|
||||||
else if (rar.includes("mil-spec") || rar.includes("uncommon"))
|
|
||||||
rarityMapped = "uncommon";
|
|
||||||
}
|
|
||||||
|
|
||||||
const newItem = new Item({
|
|
||||||
name: item.name,
|
|
||||||
description: `Listed from Steam inventory`,
|
|
||||||
image: item.image,
|
|
||||||
game: game,
|
|
||||||
category: categoryMapped,
|
|
||||||
rarity: rarityMapped,
|
|
||||||
wear: item.wear || null,
|
|
||||||
statTrak: item.statTrak || false,
|
|
||||||
souvenir: item.souvenir || false,
|
|
||||||
price: item.price,
|
|
||||||
seller: userId,
|
|
||||||
status: "active",
|
|
||||||
featured: false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await newItem.save();
|
|
||||||
createdItems.push(newItem);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update user balance
|
// Development mode: Mock trade creation
|
||||||
request.user.balance += totalValue;
|
if (bypassBots || !botManager.isInitialized) {
|
||||||
await request.user.save();
|
console.log(
|
||||||
|
"⚠️ DEVELOPMENT MODE: Creating mock trade (no real Steam bot)"
|
||||||
|
);
|
||||||
|
|
||||||
console.log(
|
// Generate mock verification code
|
||||||
`✅ User ${request.user.username} sold ${
|
const mockCode = Math.random()
|
||||||
items.length
|
.toString(36)
|
||||||
} items for $${totalValue.toFixed(2)}`
|
.substring(2, 8)
|
||||||
);
|
.toUpperCase();
|
||||||
|
const mockOfferId = `DEV_${Date.now()}`;
|
||||||
|
const mockTradeOfferUrl = `https://steamcommunity.com/tradeoffer/${mockOfferId}`;
|
||||||
|
|
||||||
// Broadcast to WebSocket if available
|
// Create trade record in database
|
||||||
if (fastify.websocketManager) {
|
const trade = new Trade({
|
||||||
// Update user's balance
|
offerId: mockOfferId,
|
||||||
fastify.websocketManager.sendToUser(steamId, {
|
botId: "dev-bot",
|
||||||
type: "balance_update",
|
userId: userId,
|
||||||
data: {
|
steamId: steamId,
|
||||||
balance: request.user.balance,
|
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",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Broadcast new items to marketplace
|
await trade.save();
|
||||||
fastify.websocketManager.broadcastPublic("new_items", {
|
|
||||||
count: createdItems.length,
|
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({
|
return reply.send({
|
||||||
success: true,
|
success: true,
|
||||||
message: `Successfully sold ${items.length} item${
|
message: "Trade offer created successfully",
|
||||||
items.length > 1 ? "s" : ""
|
trade: {
|
||||||
} for $${totalValue.toFixed(2)}`,
|
tradeId: trade._id,
|
||||||
itemsListed: createdItems.length,
|
offerId: tradeResult.offerId,
|
||||||
totalEarned: totalValue,
|
verificationCode: tradeResult.code,
|
||||||
newBalance: request.user.balance,
|
tradeOfferUrl: tradeResult.tradeOfferUrl,
|
||||||
|
itemCount: items.length,
|
||||||
|
totalValue,
|
||||||
|
status: "pending",
|
||||||
|
botId: tradeResult.botId,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error selling items:", 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({
|
return reply.status(500).send({
|
||||||
success: false,
|
success: false,
|
||||||
message: "Failed to process sale. Please try again.",
|
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",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import SteamTotp from "steam-totp";
|
|||||||
import { EventEmitter } from "events";
|
import { EventEmitter } from "events";
|
||||||
import { SocksProxyAgent } from "socks-proxy-agent";
|
import { SocksProxyAgent } from "socks-proxy-agent";
|
||||||
import HttpsProxyAgent from "https-proxy-agent";
|
import HttpsProxyAgent from "https-proxy-agent";
|
||||||
|
import wsManager from "../utils/websocket.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Steam Bot Service with Multi-Bot Support, Proxies, and Verification Codes
|
* Steam Bot Service with Multi-Bot Support, Proxies, and Verification Codes
|
||||||
@@ -223,7 +224,9 @@ class SteamBotInstance extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create trade offer with verification code
|
* Create a trade offer
|
||||||
|
* @param {Object} options - Trade offer options
|
||||||
|
* @returns {Promise<Object>} Trade offer result
|
||||||
*/
|
*/
|
||||||
async createTradeOffer(options) {
|
async createTradeOffer(options) {
|
||||||
if (!this.isReady) {
|
if (!this.isReady) {
|
||||||
@@ -235,6 +238,7 @@ class SteamBotInstance extends EventEmitter {
|
|||||||
itemsToReceive,
|
itemsToReceive,
|
||||||
verificationCode,
|
verificationCode,
|
||||||
metadata = {},
|
metadata = {},
|
||||||
|
userId,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
if (!tradeUrl) throw new Error("Trade URL is required");
|
if (!tradeUrl) throw new Error("Trade URL is required");
|
||||||
@@ -247,6 +251,19 @@ class SteamBotInstance extends EventEmitter {
|
|||||||
`📤 Bot ${this.botId} creating trade offer for ${itemsToReceive.length} items (Code: ${verificationCode})`
|
`📤 Bot ${this.botId} creating trade offer for ${itemsToReceive.length} items (Code: ${verificationCode})`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Notify user that trade is being created
|
||||||
|
if (userId) {
|
||||||
|
wsManager.sendToUser(userId, {
|
||||||
|
type: "trade_creating",
|
||||||
|
data: {
|
||||||
|
verificationCode,
|
||||||
|
itemCount: itemsToReceive.length,
|
||||||
|
botId: this.botId,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const offer = this.manager.createOffer(tradeUrl);
|
const offer = this.manager.createOffer(tradeUrl);
|
||||||
|
|
||||||
@@ -263,6 +280,20 @@ class SteamBotInstance extends EventEmitter {
|
|||||||
err.message
|
err.message
|
||||||
);
|
);
|
||||||
this.errorCount++;
|
this.errorCount++;
|
||||||
|
|
||||||
|
// Notify user of error
|
||||||
|
if (userId) {
|
||||||
|
wsManager.sendToUser(userId, {
|
||||||
|
type: "trade_error",
|
||||||
|
data: {
|
||||||
|
verificationCode,
|
||||||
|
error: err.message,
|
||||||
|
botId: this.botId,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return reject(err);
|
return reject(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,6 +301,9 @@ class SteamBotInstance extends EventEmitter {
|
|||||||
`✅ Bot ${this.botId} trade sent: ${offer.id} (Code: ${verificationCode})`
|
`✅ Bot ${this.botId} trade sent: ${offer.id} (Code: ${verificationCode})`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Get trade offer URL
|
||||||
|
const tradeOfferUrl = `https://steamcommunity.com/tradeoffer/${offer.id}`;
|
||||||
|
|
||||||
this.activeTrades.set(offer.id, {
|
this.activeTrades.set(offer.id, {
|
||||||
id: offer.id,
|
id: offer.id,
|
||||||
status: status,
|
status: status,
|
||||||
@@ -279,23 +313,71 @@ class SteamBotInstance extends EventEmitter {
|
|||||||
metadata: metadata,
|
metadata: metadata,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
botId: this.botId,
|
botId: this.botId,
|
||||||
|
userId: userId,
|
||||||
|
tradeOfferUrl: tradeOfferUrl,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.tradeCount++;
|
this.tradeCount++;
|
||||||
this.lastTradeTime = new Date();
|
this.lastTradeTime = new Date();
|
||||||
|
|
||||||
|
// Notify user that trade was sent
|
||||||
|
if (userId) {
|
||||||
|
wsManager.sendToUser(userId, {
|
||||||
|
type: "trade_sent",
|
||||||
|
data: {
|
||||||
|
offerId: offer.id,
|
||||||
|
verificationCode,
|
||||||
|
status,
|
||||||
|
botId: this.botId,
|
||||||
|
itemCount: itemsToReceive.length,
|
||||||
|
tradeOfferUrl,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (status === "pending") {
|
if (status === "pending") {
|
||||||
this._confirmTradeOffer(offer)
|
this._confirmTradeOffer(offer)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
// Notify user that trade was confirmed
|
||||||
|
if (userId) {
|
||||||
|
wsManager.sendToUser(userId, {
|
||||||
|
type: "trade_confirmed",
|
||||||
|
data: {
|
||||||
|
offerId: offer.id,
|
||||||
|
verificationCode,
|
||||||
|
botId: this.botId,
|
||||||
|
tradeOfferUrl,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
resolve({
|
resolve({
|
||||||
offerId: offer.id,
|
offerId: offer.id,
|
||||||
botId: this.botId,
|
botId: this.botId,
|
||||||
status: "sent",
|
status: "sent",
|
||||||
verificationCode: verificationCode,
|
verificationCode: verificationCode,
|
||||||
requiresConfirmation: true,
|
requiresConfirmation: true,
|
||||||
|
tradeOfferUrl,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((confirmErr) => {
|
.catch((confirmErr) => {
|
||||||
|
// Notify user of confirmation error
|
||||||
|
if (userId) {
|
||||||
|
wsManager.sendToUser(userId, {
|
||||||
|
type: "trade_confirmation_error",
|
||||||
|
data: {
|
||||||
|
offerId: offer.id,
|
||||||
|
verificationCode,
|
||||||
|
error: confirmErr.message,
|
||||||
|
botId: this.botId,
|
||||||
|
tradeOfferUrl,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
resolve({
|
resolve({
|
||||||
offerId: offer.id,
|
offerId: offer.id,
|
||||||
botId: this.botId,
|
botId: this.botId,
|
||||||
@@ -303,6 +385,7 @@ class SteamBotInstance extends EventEmitter {
|
|||||||
verificationCode: verificationCode,
|
verificationCode: verificationCode,
|
||||||
requiresConfirmation: true,
|
requiresConfirmation: true,
|
||||||
error: confirmErr.message,
|
error: confirmErr.message,
|
||||||
|
tradeOfferUrl,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -312,6 +395,7 @@ class SteamBotInstance extends EventEmitter {
|
|||||||
status: "sent",
|
status: "sent",
|
||||||
verificationCode: verificationCode,
|
verificationCode: verificationCode,
|
||||||
requiresConfirmation: false,
|
requiresConfirmation: false,
|
||||||
|
tradeOfferUrl,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -365,26 +449,103 @@ class SteamBotInstance extends EventEmitter {
|
|||||||
tradeData.state = offer.state;
|
tradeData.state = offer.state;
|
||||||
tradeData.updatedAt = new Date();
|
tradeData.updatedAt = new Date();
|
||||||
|
|
||||||
|
const userId = tradeData.userId;
|
||||||
|
|
||||||
switch (offer.state) {
|
switch (offer.state) {
|
||||||
case TradeOfferManager.ETradeOfferState.Accepted:
|
case TradeOfferManager.ETradeOfferState.Accepted:
|
||||||
console.log(`✅ Bot ${this.botId} trade ${offer.id} ACCEPTED`);
|
console.log(`✅ Bot ${this.botId} trade ${offer.id} ACCEPTED`);
|
||||||
this.emit("tradeAccepted", offer, tradeData);
|
this.emit("tradeAccepted", offer, tradeData);
|
||||||
this.errorCount = Math.max(0, this.errorCount - 1); // Decrease error count on success
|
this.errorCount = Math.max(0, this.errorCount - 1); // Decrease error count on success
|
||||||
|
|
||||||
|
// Notify user via WebSocket
|
||||||
|
if (userId) {
|
||||||
|
wsManager.sendToUser(userId, {
|
||||||
|
type: "trade_accepted",
|
||||||
|
data: {
|
||||||
|
offerId: offer.id,
|
||||||
|
verificationCode: tradeData.verificationCode,
|
||||||
|
botId: this.botId,
|
||||||
|
itemCount: tradeData.itemsToReceive?.length || 0,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case TradeOfferManager.ETradeOfferState.Declined:
|
case TradeOfferManager.ETradeOfferState.Declined:
|
||||||
console.log(`❌ Bot ${this.botId} trade ${offer.id} DECLINED`);
|
console.log(`❌ Bot ${this.botId} trade ${offer.id} DECLINED`);
|
||||||
this.emit("tradeDeclined", offer, tradeData);
|
this.emit("tradeDeclined", offer, tradeData);
|
||||||
this.activeTrades.delete(offer.id);
|
this.activeTrades.delete(offer.id);
|
||||||
|
|
||||||
|
// Notify user via WebSocket
|
||||||
|
if (userId) {
|
||||||
|
wsManager.sendToUser(userId, {
|
||||||
|
type: "trade_declined",
|
||||||
|
data: {
|
||||||
|
offerId: offer.id,
|
||||||
|
verificationCode: tradeData.verificationCode,
|
||||||
|
botId: this.botId,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case TradeOfferManager.ETradeOfferState.Expired:
|
case TradeOfferManager.ETradeOfferState.Expired:
|
||||||
console.log(`⏰ Bot ${this.botId} trade ${offer.id} EXPIRED`);
|
console.log(`⏰ Bot ${this.botId} trade ${offer.id} EXPIRED`);
|
||||||
this.emit("tradeExpired", offer, tradeData);
|
this.emit("tradeExpired", offer, tradeData);
|
||||||
this.activeTrades.delete(offer.id);
|
this.activeTrades.delete(offer.id);
|
||||||
|
|
||||||
|
// Notify user via WebSocket
|
||||||
|
if (userId) {
|
||||||
|
wsManager.sendToUser(userId, {
|
||||||
|
type: "trade_expired",
|
||||||
|
data: {
|
||||||
|
offerId: offer.id,
|
||||||
|
verificationCode: tradeData.verificationCode,
|
||||||
|
botId: this.botId,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case TradeOfferManager.ETradeOfferState.Canceled:
|
case TradeOfferManager.ETradeOfferState.Canceled:
|
||||||
console.log(`🚫 Bot ${this.botId} trade ${offer.id} CANCELED`);
|
console.log(`🚫 Bot ${this.botId} trade ${offer.id} CANCELED`);
|
||||||
this.emit("tradeCanceled", offer, tradeData);
|
this.emit("tradeCanceled", offer, tradeData);
|
||||||
this.activeTrades.delete(offer.id);
|
this.activeTrades.delete(offer.id);
|
||||||
|
|
||||||
|
// Notify user via WebSocket
|
||||||
|
if (userId) {
|
||||||
|
wsManager.sendToUser(userId, {
|
||||||
|
type: "trade_canceled",
|
||||||
|
data: {
|
||||||
|
offerId: offer.id,
|
||||||
|
verificationCode: tradeData.verificationCode,
|
||||||
|
botId: this.botId,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TradeOfferManager.ETradeOfferState.Invalid:
|
||||||
|
console.log(`⚠️ Bot ${this.botId} trade ${offer.id} INVALID`);
|
||||||
|
this.emit("tradeInvalid", offer, tradeData);
|
||||||
|
this.activeTrades.delete(offer.id);
|
||||||
|
|
||||||
|
// Notify user via WebSocket
|
||||||
|
if (userId) {
|
||||||
|
wsManager.sendToUser(userId, {
|
||||||
|
type: "trade_invalid",
|
||||||
|
data: {
|
||||||
|
offerId: offer.id,
|
||||||
|
verificationCode: tradeData.verificationCode,
|
||||||
|
botId: this.botId,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -579,6 +740,7 @@ class SteamBotManager extends EventEmitter {
|
|||||||
tradeUrl,
|
tradeUrl,
|
||||||
itemsToReceive,
|
itemsToReceive,
|
||||||
verificationCode,
|
verificationCode,
|
||||||
|
userId,
|
||||||
metadata: {
|
metadata: {
|
||||||
...metadata,
|
...metadata,
|
||||||
userId,
|
userId,
|
||||||
@@ -592,9 +754,11 @@ class SteamBotManager extends EventEmitter {
|
|||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Return result with trade offer URL
|
||||||
return {
|
return {
|
||||||
...result,
|
...result,
|
||||||
verificationCode,
|
code: verificationCode,
|
||||||
|
tradeOfferUrl: result.tradeOfferUrl,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user