@copytrade/broker-shoonya
Version:
Shoonya (Finvasia) broker plugin for @copytrade/unified-broker
369 lines • 14.9 kB
JavaScript
"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