@copytrade/unified-broker
Version:
Unified broker interface library for Indian stock market brokers with plugin architecture
493 lines • 21.1 kB
JavaScript
"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