using System.Net.Http.Headers; using System.Text; using Managing.Api.Models.Requests; using Managing.Application.Abstractions; using Managing.Application.Abstractions.Services; using Managing.Application.Shared; using Managing.Application.Trading.Commands; using Managing.Domain.MoneyManagements; using Managing.Domain.Trades; using Managing.Infrastructure.Evm.Models.Privy; using MediatR; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using static Managing.Common.Enums; namespace Managing.Api.Controllers; /// /// Controller for trading operations such as opening and closing positions, and retrieving trade information. /// Requires authorization for access. /// [ApiController] [Authorize] [Route("[controller]")] public class TradingController : BaseController { private readonly ICommandHandler _openTradeCommandHandler; private readonly ICommandHandler _closeTradeCommandHandler; private readonly ITradingService _tradingService; private readonly IMoneyManagementService _moneyManagementService; private readonly IMediator _mediator; private readonly ILogger _logger; private readonly IAdminConfigurationService _adminService; private readonly IAccountService _accountService; private readonly IHttpClientFactory _httpClientFactory; private readonly IConfiguration _configuration; /// /// Initializes a new instance of the class. /// /// Logger for logging information. /// Command handler for opening trades. /// Command handler for closing trades. /// Service for trading operations. /// Mediator for handling commands and requests. /// Service for checking admin privileges. /// Service for account operations. /// HTTP client factory for making web requests. /// Application configuration. public TradingController( ILogger logger, ICommandHandler openTradeCommandHandler, ICommandHandler closeTradeCommandHandler, ITradingService tradingService, IMediator mediator, IMoneyManagementService moneyManagementService, IUserService userService, IAdminConfigurationService adminService, IAccountService accountService, IHttpClientFactory httpClientFactory, IConfiguration configuration) : base(userService) { _logger = logger; _openTradeCommandHandler = openTradeCommandHandler; _closeTradeCommandHandler = closeTradeCommandHandler; _tradingService = tradingService; _mediator = mediator; _moneyManagementService = moneyManagementService; _adminService = adminService; _accountService = accountService; _httpClientFactory = httpClientFactory; _configuration = configuration; } /// /// Retrieves a specific trade by account name, ticker, and exchange order ID. /// /// The name of the account. /// The ticker symbol of the trade. /// The exchange order ID of the trade. /// The requested trade. [HttpGet("GetTrade")] public async Task> GetTrade(string accountName, Ticker ticker, string exchangeOrderId) { var result = await _mediator.Send(new GetTradeCommand(accountName, exchangeOrderId, ticker)); return Ok(result); } /// /// Retrieves a list of trades for a given account and ticker. /// /// The name of the account. /// The ticker symbol of the trades. /// A list of trades. [HttpGet("GetTrades")] public async Task> GetTrades(string accountName, Ticker ticker) { var result = await _mediator.Send(new GetTradesCommand(ticker, accountName)); return Ok(result); } /// /// Closes a position identified by its unique identifier. /// /// The unique identifier of the position to close. /// The closed position. [HttpPost("ClosePosition")] public async Task> ClosePosition(Guid identifier) { var position = await _tradingService.GetPositionByIdentifierAsync(identifier); var result = await _closeTradeCommandHandler.Handle(new ClosePositionCommand(position, position.AccountId)); return Ok(result); } /// /// Opens a new position based on the provided parameters. /// /// The name of the account to open the position for. /// The name of the money management strategy to use. /// The direction of the trade (Buy or Sell). /// The ticker symbol for the trade. /// The risk level for the trade. /// Indicates whether the trade is for paper trading. /// The money management strategy details (optional). /// The opening price for the trade (optional). /// The opened position. [HttpPost("OpenPosition")] public async Task> Trade( string accountName, string moneyManagementName, TradeDirection direction, Ticker ticker, RiskLevel riskLevel, bool isForPaperTrading, MoneyManagement? moneyManagement = null, decimal? openPrice = null) { if (string.IsNullOrEmpty(accountName)) { throw new ArgumentException($"'{nameof(accountName)}' cannot be null or empty.", nameof(accountName)); } if (string.IsNullOrEmpty(moneyManagementName) && moneyManagement == null) { throw new ArgumentException($"'{nameof(moneyManagementName)}' cannot be null or empty.", nameof(moneyManagementName)); } var user = await GetUser(); if (moneyManagement != null) { moneyManagement = await _moneyManagementService.GetMoneyMangement(user, moneyManagementName); } var command = new OpenPositionRequest( accountName, moneyManagement, direction, ticker, PositionInitiator.User, DateTime.UtcNow, user, 100m, // Default trading balance for user-initiated trades isForPaperTrading: isForPaperTrading, price: openPrice); var result = await _openTradeCommandHandler.Handle(command); return Ok(result); } /// /// Initializes a Privy wallet address for the user. /// Only admins can initialize any address, regular users can only initialize their own addresses. /// /// The public address of the Privy wallet to initialize. /// The initialization response containing success status and transaction hashes. [HttpPost("InitPrivyWallet")] public async Task> InitPrivyWallet([FromBody] string publicAddress) { if (string.IsNullOrEmpty(publicAddress)) { return BadRequest("Public address cannot be null or empty."); } try { var user = await GetUser(); if (user == null) { return Unauthorized("User not found"); } // Check if user has permission to initialize this address if (!await CanUserInitializeAddress(user.Name, publicAddress)) { return Forbid( "You don't have permission to initialize this wallet address. You can only initialize your own wallet addresses."); } var result = await _tradingService.InitPrivyWallet(publicAddress, TradingExchanges.GmxV2); return Ok(result); } catch (Exception ex) { _logger.LogError(ex, "Error initializing Privy wallet address: {Address}", publicAddress); return StatusCode(500, new PrivyInitAddressResponse { Success = false, Error = "An error occurred while initializing the Privy wallet address." }); } } /// /// Checks if the user can initialize the given public address. /// Admins can initialize any address, regular users can only initialize their own addresses. /// /// The username to check /// The public address to initialize /// True if the user can initialize the address, false otherwise private async Task CanUserInitializeAddress(string userName, string publicAddress) { // Admin users can initialize any address if (await _adminService.IsUserAdminAsync(userName)) { _logger.LogInformation("Admin user {UserName} initializing address {Address}", userName, publicAddress); return true; } try { // Regular users can only initialize their own addresses // Check if the address belongs to one of the user's accounts var account = await _accountService.GetAccountByKey(publicAddress, true, false); if (account?.User?.Name == userName) { _logger.LogInformation("User {UserName} initializing their own address {Address}", userName, publicAddress); return true; } _logger.LogWarning("User {UserName} attempted to initialize address {Address} that doesn't belong to them", userName, publicAddress); return false; } catch (Exception ex) { _logger.LogWarning(ex, "Unable to verify ownership of address {Address} for user {UserName}", publicAddress, userName); return false; } } /// /// Submits a request for a new indicator to be developed via N8n webhook. /// /// The indicator request details including name, strategy, documentation, and requester information. /// A success response indicating the request was submitted. [HttpPost("RequestIndicator")] public async Task> RequestIndicator([FromBody] IndicatorRequestDto request) { if (request == null) { return BadRequest("Request cannot be null."); } if (string.IsNullOrWhiteSpace(request.IndicatorName)) { return BadRequest("Indicator name is required."); } if (string.IsNullOrWhiteSpace(request.StrategyDescription)) { return BadRequest("Strategy is required."); } if (string.IsNullOrWhiteSpace(request.RequesterName)) { return BadRequest("Requester name is required."); } try { var webhookUrl = _configuration["N8n:IndicatorRequestWebhookUrl"]; if (string.IsNullOrEmpty(webhookUrl)) { _logger.LogError("N8n indicator request webhook URL is not configured"); return StatusCode(500, new { Success = false, Error = "Webhook URL is not configured." }); } var httpClient = _httpClientFactory.CreateClient(); // Add basic authentication if credentials are provided var username = _configuration["N8n:Username"]; var password = _configuration["N8n:Password"]; if (!string.IsNullOrEmpty(username) && !string.IsNullOrEmpty(password)) { var credentials = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{username}:{password}")); httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", credentials); } _logger.LogInformation( "Submitting indicator request: {IndicatorName} - {Strategy} by {Requester}", request.IndicatorName, request.StrategyDescription, request.RequesterName); // Send as JSON payload var response = await httpClient.PostAsJsonAsync(webhookUrl, request); if (response.IsSuccessStatusCode) { _logger.LogInformation( "Successfully submitted indicator request: {IndicatorName} by {Requester}", request.IndicatorName, request.RequesterName); return Ok(new { Success = true, Message = "Indicator request submitted successfully.", IndicatorName = request.IndicatorName, Strategy = request.StrategyDescription, Requester = request.RequesterName }); } else { var responseContent = await response.Content.ReadAsStringAsync(); _logger.LogError( "Failed to submit indicator request. Status: {StatusCode}, Response: {Response}", response.StatusCode, responseContent); return StatusCode(500, new { Success = false, Error = $"Failed to submit indicator request. Status: {response.StatusCode}" }); } } catch (Exception ex) { _logger.LogError(ex, "Error submitting indicator request: {IndicatorName}", request.IndicatorName); return StatusCode(500, new { Success = false, Error = "An error occurred while submitting the indicator request." }); } } }