UNPKG

@copytrade/unified-broker

Version:

Unified broker interface library for Indian stock market brokers with plugin architecture

493 lines 21.1 kB
"use strict"; /** * Unified Fyers Service * Implements the new IUnifiedBrokerService interface with all business logic encapsulated * Handles OAuth flow, token refresh, profile fetching, expiry management, and provides standardized responses */ Object.defineProperty(exports, "__esModule", { value: true }); exports.UnifiedFyersService = void 0; const UnifiedBrokerResponse_1 = require("../../interfaces/UnifiedBrokerResponse"); const fyersService_1 = require("../../services/fyersService"); class UnifiedFyersService { constructor() { this.isConnectedFlag = false; this.accountInfo = null; this.tokenInfo = null; this.currentCredentials = null; this.fyersService = new fyersService_1.FyersService(); } getBrokerName() { return 'fyers'; } /** * Connect to Fyers broker - initiates OAuth flow * Returns OAuth URL for user authentication */ async connect(credentials) { try { const fyersCredentials = credentials; this.currentCredentials = fyersCredentials; console.log('🔄 Initiating Fyers OAuth flow...'); // Validate required credentials if (!fyersCredentials.clientId || !fyersCredentials.secretKey) { return UnifiedBrokerResponse_1.UnifiedResponseHelper.createErrorResponse('Client ID and Secret Key are required for Fyers authentication', 'VALIDATION_ERROR', 'INACTIVE', 'REAUTH_REQUIRED'); } // Set default redirect URI if not provided if (!fyersCredentials.redirectUri) { fyersCredentials.redirectUri = process.env.FYERS_REDIRECT_URI || 'http://localhost:3001/api/broker/oauth/callback'; } // Generate OAuth URL for user authentication const authUrl = this.fyersService.generateAuthUrl(fyersCredentials); if (authUrl) { console.log('✅ Fyers OAuth URL generated successfully'); console.log(`🔗 OAuth URL: ${authUrl}`); return UnifiedBrokerResponse_1.UnifiedResponseHelper.createOAuthResponse(authUrl, 'Please complete OAuth authentication. You will be redirected back after authorization.'); } else { console.error('❌ Failed to generate Fyers OAuth URL'); return UnifiedBrokerResponse_1.UnifiedResponseHelper.createErrorResponse('Failed to generate OAuth URL for Fyers. Please check your credentials.', 'BROKER_ERROR', 'INACTIVE', 'REAUTH_REQUIRED'); } } catch (error) { console.error('🚨 Fyers connection error:', error); return UnifiedBrokerResponse_1.UnifiedResponseHelper.createErrorResponse(error.message || 'Failed to initiate Fyers OAuth flow', 'BROKER_ERROR', 'INACTIVE', 'REAUTH_REQUIRED'); } } /** * Complete Fyers OAuth flow with auth code * Generates access token, fetches profile, and sets up account */ async completeOAuth(authCode, credentials) { try { const fyersCredentials = credentials; console.log('🔄 Completing Fyers OAuth with auth code...'); // Generate access token from auth code const tokenResponse = await this.fyersService.generateAccessToken(authCode, fyersCredentials); if (tokenResponse.success) { console.log('✅ Fyers access token generated successfully'); // Fetch user profile to get real account information let realAccountId = tokenResponse.accountId || fyersCredentials.clientId; let realUserName = tokenResponse.accountId || fyersCredentials.clientId; try { const profileResponse = await this.fyersService.getProfile(); if (profileResponse && profileResponse.data) { realAccountId = profileResponse.data.fy_id || profileResponse.data.id || profileResponse.data.user_id || realAccountId; realUserName = profileResponse.data.name || profileResponse.data.display_name || realAccountId; console.log(`✅ Fyers profile fetched - Account ID: ${realAccountId}, User Name: ${realUserName}`); } } catch (profileError) { console.warn('⚠️ Failed to fetch Fyers profile, using fallback values:', profileError.message); } // Set up account information this.accountInfo = { accountId: realAccountId, userName: realUserName, email: '', // Fyers doesn't provide email in profile brokerDisplayName: 'Fyers', exchanges: ['NSE', 'BSE', 'MCX'], // Fyers supported exchanges products: ['CNC', 'INTRADAY', 'MARGIN', 'CO', 'BO'] // Fyers product types }; // Set up token information with expiry const expiryTime = tokenResponse.expiryTime || new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(); this.tokenInfo = { accessToken: tokenResponse.accessToken, refreshToken: tokenResponse.refreshToken, expiryTime: expiryTime, isExpired: false, canRefresh: !!tokenResponse.refreshToken }; this.isConnectedFlag = true; console.log('✅ Fyers OAuth completion successful'); return UnifiedBrokerResponse_1.UnifiedResponseHelper.createSuccessResponse('Fyers OAuth authentication completed successfully', 'ACTIVE', 'OAUTH_COMPLETION', this.accountInfo, this.tokenInfo); } else { console.error('❌ Fyers OAuth completion failed:', tokenResponse.message); // Check if it's an auth code expiry issue const isAuthCodeExpired = tokenResponse.message && (tokenResponse.message.includes('expired') || tokenResponse.message.includes('invalid auth code') || tokenResponse.message.includes('code has expired')); if (isAuthCodeExpired) { return UnifiedBrokerResponse_1.UnifiedResponseHelper.createErrorResponse('Auth code has expired. Please try again.', 'AUTH_CODE_EXPIRED', 'PROCEED_TO_OAUTH', 'OAUTH_REQUIRED'); } else { return UnifiedBrokerResponse_1.UnifiedResponseHelper.createErrorResponse(tokenResponse.message || 'Access token generation failed. Please try again.', 'AUTH_FAILED', 'PROCEED_TO_OAUTH', 'OAUTH_REQUIRED'); } } } catch (error) { console.error('🚨 Fyers OAuth completion error:', error); return UnifiedBrokerResponse_1.UnifiedResponseHelper.createErrorResponse(error.message || 'OAuth completion failed. Please try again.', 'BROKER_ERROR', 'PROCEED_TO_OAUTH', 'OAUTH_REQUIRED'); } } /** * Refresh Fyers access token using refresh token */ async refreshToken(credentials) { if (!this.tokenInfo || !this.tokenInfo.refreshToken) { return UnifiedBrokerResponse_1.UnifiedResponseHelper.createErrorResponse('No refresh token available for Fyers', 'REFRESH_TOKEN_EXPIRED', 'PROCEED_TO_OAUTH', 'OAUTH_REQUIRED'); } try { console.log('🔄 Refreshing Fyers access token...'); // Use the FyersService to refresh the token const refreshResult = await this.fyersService.refreshAccessToken(this.tokenInfo.refreshToken); if (refreshResult.success && refreshResult.accessToken) { // Update token information this.tokenInfo = { accessToken: refreshResult.accessToken, refreshToken: refreshResult.refreshToken || this.tokenInfo.refreshToken, expiryTime: refreshResult.expiryTime || new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), isExpired: false, canRefresh: true }; console.log('✅ Fyers access token refreshed successfully'); return UnifiedBrokerResponse_1.UnifiedResponseHelper.createSuccessResponse('Fyers access token refreshed successfully', 'ACTIVE', 'TOKEN_REFRESH', this.accountInfo || undefined, this.tokenInfo || undefined); } else { console.error('❌ Fyers token refresh failed:', refreshResult.message); // If refresh fails, user needs to re-authenticate return UnifiedBrokerResponse_1.UnifiedResponseHelper.createErrorResponse(refreshResult.message || 'Token refresh failed. Please re-authenticate.', 'REFRESH_TOKEN_EXPIRED', 'PROCEED_TO_OAUTH', 'OAUTH_REQUIRED'); } } catch (error) { console.error('🚨 Fyers token refresh error:', error); return UnifiedBrokerResponse_1.UnifiedResponseHelper.createErrorResponse('Token refresh failed. Please re-authenticate.', 'REFRESH_TOKEN_EXPIRED', 'PROCEED_TO_OAUTH', 'OAUTH_REQUIRED'); } } /** * Validate current Fyers session and check token expiry */ async validateSession(credentials) { if (!this.isConnectedFlag || !this.tokenInfo) { return { isValid: false, accountStatus: 'INACTIVE', message: 'No active Fyers session', errorType: 'AUTH_FAILED' }; } // Check if token is expired if (this.tokenInfo.expiryTime) { const now = new Date(); const expiryTime = new Date(this.tokenInfo.expiryTime); if (now > expiryTime) { this.tokenInfo.isExpired = true; if (this.tokenInfo.canRefresh) { return { isValid: false, accountStatus: 'REFRESH_REQUIRED', message: 'Fyers token has expired but can be refreshed', errorType: 'TOKEN_EXPIRED', tokenInfo: this.tokenInfo }; } else { return { isValid: false, accountStatus: 'PROCEED_TO_OAUTH', message: 'Fyers token has expired. Re-authentication required.', errorType: 'TOKEN_EXPIRED' }; } } } try { // Validate session with Fyers API const isValid = await this.fyersService.validateSession(); if (isValid) { return { isValid: true, accountStatus: 'ACTIVE', message: 'Fyers session is valid', tokenInfo: this.tokenInfo }; } else { // Session is no longer valid this.isConnectedFlag = false; return { isValid: false, accountStatus: 'PROCEED_TO_OAUTH', message: 'Fyers session is no longer valid', errorType: 'TOKEN_EXPIRED' }; } } catch (error) { console.error('🚨 Fyers session validation error:', error); return { isValid: false, accountStatus: 'INACTIVE', message: 'Failed to validate Fyers session', errorType: 'NETWORK_ERROR' }; } } /** * Disconnect from Fyers */ async disconnect() { try { if (this.isConnectedFlag) { await this.fyersService.logout(); } // Reset all state this.isConnectedFlag = false; this.accountInfo = null; this.tokenInfo = null; this.currentCredentials = null; console.log('✅ Fyers disconnected successfully'); return true; } catch (error) { console.error('🚨 Fyers disconnect error:', error); // Reset state even if logout fails this.isConnectedFlag = false; this.accountInfo = null; this.tokenInfo = null; this.currentCredentials = null; return false; } } /** * Get current account information */ getAccountInfo() { return this.accountInfo; } /** * Get current token information */ getTokenInfo() { return this.tokenInfo; } /** * Check if currently connected */ isConnected() { return this.isConnectedFlag && !this.isTokenExpired(); } /** * Get current account status */ getAccountStatus() { if (!this.isConnectedFlag) { return 'INACTIVE'; } if (this.isTokenExpired()) { return this.tokenInfo?.canRefresh ? 'REFRESH_REQUIRED' : 'PROCEED_TO_OAUTH'; } return 'ACTIVE'; } /** * Check if token is expired */ isTokenExpired() { if (!this.tokenInfo || !this.tokenInfo.expiryTime) { return false; } const now = new Date(); const expiryTime = new Date(this.tokenInfo.expiryTime); return now > expiryTime; } // Trading operations - delegate to existing service async placeOrder(orderRequest) { if (!this.isConnected()) { throw new Error('Not connected to Fyers. Please authenticate first.'); } // Transform unified order request to Fyers format const fyersOrderRequest = { symbol: this.formatSymbolForFyers(orderRequest.symbol, orderRequest.exchange), qty: Math.abs(orderRequest.quantity), // Ensure positive quantity type: this.mapOrderType(orderRequest.orderType), side: orderRequest.action, productType: this.mapProductType(orderRequest.productType), limitPrice: orderRequest.price || 0, stopPrice: orderRequest.triggerPrice || 0, validity: orderRequest.validity || 'DAY', disclosedQty: 0, offlineOrder: false }; try { console.log('🔄 Placing Fyers order with request:', JSON.stringify(fyersOrderRequest, null, 2)); const fyersResponse = await this.fyersService.placeOrder(fyersOrderRequest); // Transform Fyers response to unified format if (fyersResponse.s === 'ok') { return { success: true, message: 'Order placed successfully', data: { brokerOrderId: fyersResponse.id, orderId: fyersResponse.id, status: 'PLACED' } }; } else { return { success: false, message: fyersResponse.message || 'Order placement failed', data: null }; } } catch (error) { console.error('🚨 Fyers order placement error:', error); return { success: false, message: error.message || 'Order placement failed', data: null }; } } async cancelOrder(orderId) { if (!this.isConnected()) { throw new Error('Not connected to Fyers. Please authenticate first.'); } try { console.log('🚫 Cancelling Fyers order:', orderId); const fyersResponse = await this.fyersService.cancelOrder(orderId); // Transform Fyers response to unified format if (fyersResponse.s === 'ok') { return { success: true, message: 'Order cancelled successfully', data: { orderId: orderId, status: 'CANCELLED' } }; } else { return { success: false, message: fyersResponse.message || 'Order cancellation failed', data: null }; } } catch (error) { console.error('🚨 Fyers order cancellation error:', error); return { success: false, message: error.message || 'Order cancellation failed', data: null }; } } async modifyOrder(orderId, modifications) { if (!this.isConnected()) { throw new Error('Not connected to Fyers. Please authenticate first.'); } try { console.log('✏️ Modifying Fyers order:', orderId, modifications); const fyersResponse = await this.fyersService.modifyOrder(orderId, modifications); // Transform Fyers response to unified format if (fyersResponse.s === 'ok') { return { success: true, message: 'Order modified successfully', data: { orderId: orderId, status: 'MODIFIED' } }; } else { return { success: false, message: fyersResponse.message || 'Order modification failed', data: null }; } } catch (error) { console.error('🚨 Fyers order modification error:', error); return { success: false, message: error.message || 'Order modification failed', data: null }; } } formatSymbolForFyers(symbol, exchange) { // Fyers expects format: EXCHANGE:SYMBOL-SUFFIX // e.g., NSE:TCS-EQ, BSE:TCS if (symbol.includes(':')) { // Already formatted return symbol; } // Validate exchange parameter if (!exchange || exchange === 'undefined') { throw new Error('Exchange is required for Fyers symbol formatting'); } // Format based on exchange if (exchange === 'NSE') { // NSE equity symbols require -EQ suffix return `${exchange}:${symbol}-EQ`; } else if (exchange === 'BSE') { // BSE symbols don't require suffix return `${exchange}:${symbol}`; } else { // Default format for other exchanges return `${exchange}:${symbol}`; } } mapProductType(productType) { const mapping = { 'CNC': 'CNC', 'MIS': 'INTRADAY', 'NRML': 'MARGIN', 'BO': 'BO', 'CO': 'CO' }; return mapping[productType] || 'CNC'; // Default to CNC } mapOrderType(orderType) { const mapping = { 'MARKET': 'MARKET', 'LIMIT': 'LIMIT', 'SL-LIMIT': 'SL', 'SL-MARKET': 'SL-M' }; return mapping[orderType] || 'MARKET'; // Default to MARKET } async getOrderStatus(accountId, orderId) { if (!this.isConnected()) { throw new Error('Not connected to Fyers. Please authenticate first.'); } // Use getOrderBook and filter by orderId const orderBook = await this.fyersService.getOrderBook(); return orderBook.find((order) => order.id === orderId) || null; } async getOrderHistory(accountId) { if (!this.isConnected()) { throw new Error('Not connected to Fyers. Please authenticate first.'); } return this.fyersService.getOrderBook(); } async getPositions(accountId) { if (!this.isConnected()) { throw new Error('Not connected to Fyers. Please authenticate first.'); } return this.fyersService.getPositions(); } async getQuote(symbol, exchange) { if (!this.isConnected()) { throw new Error('Not connected to Fyers. Please authenticate first.'); } const fullSymbol = `${exchange}:${symbol}`; const quotes = await this.fyersService.getQuotes([fullSymbol]); return quotes.length > 0 ? quotes[0] : null; } async searchSymbols(query, exchange) { if (!this.isConnected()) { throw new Error('Not connected to Fyers. Please authenticate first.'); } return this.fyersService.searchScrip(exchange, query); } } exports.UnifiedFyersService = UnifiedFyersService; //# sourceMappingURL=UnifiedFyersService.js.map