UNPKG

@copytrade/broker-shoonya

Version:

Shoonya (Finvasia) broker plugin for @copytrade/unified-broker

369 lines 14.9 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ShoonyaService = void 0; const crypto_1 = __importDefault(require("crypto")); const axios_1 = __importDefault(require("axios")); const otplib_1 = require("otplib"); class ShoonyaService { constructor() { this.baseUrl = 'https://api.shoonya.com/NorenWClientTP'; this.sessionToken = null; this.userId = null; } generateSHA256Hash(input) { return crypto_1.default.createHash('sha256').update(input).digest('hex'); } generateTOTP(secret) { try { // Generate TOTP using the secret key const token = otplib_1.authenticator.generate(secret); console.log('🔐 Generated TOTP:', token); return token; } catch (error) { console.error('🚨 TOTP generation error:', error); throw new Error('Failed to generate TOTP'); } } async makeRequest(endpoint, data) { try { // Shoonya API expects data as jData parameter in form-encoded format (raw string) const jsonData = JSON.stringify(data); console.log('🔍 Sending jData:', jsonData); const formBody = `jData=${jsonData}`; console.log('🔍 Form body:', formBody); const response = await axios_1.default.post(`${this.baseUrl}/${endpoint}`, formBody, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, timeout: 30000, }); return response.data; } catch (error) { console.error(`🚨 Shoonya API Error [${endpoint}]:`, error.message); if (error.response) { console.error('🚨 Response status:', error.response.status); console.error('🚨 Response data:', error.response.data); } throw new Error(`Shoonya API request failed: ${error.message}`); } } async makeAuthenticatedRequest(endpoint, data) { try { // For authenticated requests, pass session token as jKey parameter const jsonData = JSON.stringify(data); console.log('🔍 Sending authenticated jData:', jsonData); const formBody = `jData=${jsonData}&jKey=${this.sessionToken}`; console.log('🔍 Authenticated form body:', formBody); const response = await axios_1.default.post(`${this.baseUrl}/${endpoint}`, formBody, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, timeout: 30000, }); return response.data; } catch (error) { console.error(`🚨 Shoonya API Error [${endpoint}]:`, error.message); if (error.response) { console.error('🚨 Response status:', error.response.status); console.error('🚨 Response data:', error.response.data); console.error('🚨 Response headers:', error.response.headers); // If it's a 400 error, the response data usually contains the actual error if (error.response.status === 400 && error.response.data) { throw new Error(`Shoonya API Error: ${JSON.stringify(error.response.data)}`); } } throw new Error(`Shoonya API request failed: ${error.message}`); } } async login(credentials) { try { // Hash the password with SHA256 const hashedPassword = this.generateSHA256Hash(credentials.password); // Generate TOTP from the secret key const currentTOTP = this.generateTOTP(credentials.totpKey); const loginData = { uid: credentials.userId, pwd: hashedPassword, factor2: currentTOTP, vc: credentials.vendorCode, appkey: this.generateSHA256Hash(`${credentials.userId}|${credentials.apiSecret}`), imei: credentials.imei, source: 'API', apkversion: '1.0.0', // Required field that was missing }; console.log('🔐 Attempting Shoonya login for user:', credentials.userId); console.log('🔍 Login data:', { uid: loginData.uid, vc: loginData.vc, imei: loginData.imei, source: loginData.source, hasPassword: !!loginData.pwd, hasAppkey: !!loginData.appkey, hasFactor2: !!loginData.factor2 }); const response = await this.makeRequest('QuickAuth', loginData); if (response.stat === 'Ok' && response.susertoken) { this.sessionToken = response.susertoken; this.userId = credentials.userId; // Store userId for logout console.log('✅ Shoonya login successful'); return response; } else { console.error('❌ Shoonya login failed:', response.emsg || 'Unknown error'); throw new Error(response.emsg || 'Login failed'); } } catch (error) { console.error('🚨 Shoonya login error:', error.message); throw error; } } async logout(userId) { if (!this.sessionToken && !userId) { console.log('⚠️ No active session to logout from'); return { stat: 'Ok', message: 'No active session' }; } try { console.log('🔄 Logging out from Shoonya...'); const logoutData = { uid: userId || this.userId || '', // Use provided userId or stored userId jKey: this.sessionToken, // Include session token for logout }; const response = await this.makeRequest('Logout', logoutData); this.sessionToken = null; this.userId = null; if (response.stat === 'Ok') { console.log('✅ Shoonya logout successful'); return response; } else { console.log('⚠️ Shoonya logout response:', response.emsg || 'Unknown response'); return response; } } catch (error) { console.error('🚨 Shoonya logout error:', error.message); this.sessionToken = null; this.userId = null; // Don't throw error for logout - just log it return { stat: 'Ok', message: 'Logout completed with errors' }; } } async placeOrder(orderData) { if (!this.sessionToken) { throw new Error('Not logged in to Shoonya. Please login first.'); } try { // Ensure trading symbol is in correct format for NSE let tradingSymbol = orderData.tradingSymbol; if (orderData.exchange === 'NSE' && !tradingSymbol.includes('-EQ')) { tradingSymbol = `${tradingSymbol}-EQ`; } const requestData = { uid: orderData.userId, actid: orderData.userId, exch: orderData.exchange, tsym: tradingSymbol, qty: orderData.quantity.toString(), dscqty: orderData.discloseQty.toString(), prc: orderData.priceType === 'MKT' ? '0' : orderData.price.toString(), prd: orderData.productType, trantype: orderData.buyOrSell, prctyp: orderData.priceType, ret: orderData.retention || 'DAY', remarks: orderData.remarks || '', // Add additional fields that might be required ordersource: 'API', }; // Add trigger price for stop loss orders if (orderData.triggerPrice && (orderData.priceType === 'SL-LMT' || orderData.priceType === 'SL-MKT')) { requestData.trgprc = orderData.triggerPrice.toString(); } console.log('📊 Placing Shoonya order:', { symbol: orderData.tradingSymbol, action: orderData.buyOrSell, quantity: orderData.quantity, type: orderData.priceType, }); console.log('🔍 Full Shoonya order request data:', requestData); const response = await this.makeAuthenticatedRequest('PlaceOrder', requestData); if (response.stat === 'Ok') { console.log('✅ Shoonya order placed successfully:', response.norenordno); return response; } else { console.error('❌ Shoonya order placement failed:', response.emsg); throw new Error(response.emsg || 'Order placement failed'); } } catch (error) { console.error('🚨 Shoonya place order error:', error.message); throw error; } } async getOrderBook(userId) { if (!this.sessionToken) { throw new Error('Not logged in to Shoonya. Please login first.'); } try { const response = await this.makeAuthenticatedRequest('OrderBook', { uid: userId, actid: userId, }); return response; } catch (error) { console.error('🚨 Shoonya get order book error:', error.message); throw error; } } async getOrderStatus(userId, orderNumber) { if (!this.sessionToken) { throw new Error('Not logged in to Shoonya. Please login first.'); } try { console.log(`📊 Checking order status for order: ${orderNumber}`); const response = await this.makeAuthenticatedRequest('SingleOrdStatus', { uid: userId, actid: userId, norenordno: orderNumber, exch: 'NSE' }); if (response && response.stat === 'Ok') { console.log(`📊 Order ${orderNumber} status: ${response.status}`); return { stat: 'Ok', orderNumber: response.norenordno, status: response.status, symbol: response.tsym, quantity: response.qty, price: response.prc, executedQuantity: response.fillshares || '0', averagePrice: response.avgprc || '0', rejectionReason: response.rejreason || '', orderTime: response.norentm, updateTime: response.exch_tm, rawOrder: response }; } else { console.log(`⚠️ Failed to get order status: ${response?.emsg || 'Unknown error'}`); return { stat: 'Not_Ok', emsg: response?.emsg || 'Failed to get order status' }; } } catch (error) { console.error('🚨 Shoonya get order status error:', error.message); throw error; } } async getPositions(userId) { if (!this.sessionToken) { throw new Error('Not logged in to Shoonya. Please login first.'); } try { const response = await this.makeRequest('PositionBook', { uid: userId, actid: userId, token: this.sessionToken, }); return response; } catch (error) { console.error('🚨 Shoonya get positions error:', error.message); throw error; } } async searchScrip(exchange, searchText) { if (!this.sessionToken) { throw new Error('Not logged in to Shoonya. Please login first.'); } try { console.log(`🔍 Shoonya searchScrip called:`, { exchange, searchText, hasSessionToken: !!this.sessionToken }); const response = await this.makeRequest('SearchScrip', { uid: this.userId, exch: exchange, stext: searchText, token: this.sessionToken, }); console.log(`📊 Shoonya searchScrip response:`, { responseType: typeof response, isArray: Array.isArray(response), length: Array.isArray(response) ? response.length : 'N/A', response: response }); return response; } catch (error) { console.error('🚨 Shoonya search scrip error:', error.message); throw error; } } async getQuotes(exchange, token) { if (!this.sessionToken) { throw new Error('Not logged in to Shoonya. Please login first.'); } try { const response = await this.makeRequest('GetQuotes', { uid: '', exch: exchange, token: token, sessionToken: this.sessionToken, }); return response; } catch (error) { console.error('🚨 Shoonya get quotes error:', error.message); throw error; } } isLoggedIn() { return this.sessionToken !== null; } getSessionToken() { return this.sessionToken; } // Validate if the current session is still active async validateSession(userId) { if (!this.sessionToken || !userId) { return false; } try { // Use a lightweight API call to check if session is still valid // Limits is a simple endpoint that requires authentication const response = await this.makeAuthenticatedRequest('Limits', { uid: userId, actid: userId, // Account ID is usually same as user ID }); // If the call succeeds and returns 'Ok', session is valid return response.stat === 'Ok'; } catch (error) { console.log('⚠️ Session validation failed for Shoonya:', error.message); // If API call fails, session is likely expired this.sessionToken = null; this.userId = null; return false; } } /** * Get the current user ID (broker account ID) */ getUserId() { return this.userId; } } exports.ShoonyaService = ShoonyaService; //# sourceMappingURL=shoonyaService.js.map