using Managing.Application.Abstractions; using Managing.Application.Abstractions.Grains; using Managing.Application.Abstractions.Repositories; using Managing.Application.Abstractions.Services; using Managing.Application.ManageBot.Commands; using Managing.Common; using Managing.Domain.Accounts; using Managing.Domain.Bots; using Managing.Domain.Users; using MediatR; using static Managing.Common.Enums; namespace Managing.Application.ManageBot { public class StartCopyTradingCommandHandler : IRequestHandler { private readonly IAccountService _accountService; private readonly IGrainFactory _grainFactory; private readonly IBotService _botService; private readonly IKaigenService _kaigenService; private readonly IEvmManager _evmManager; public StartCopyTradingCommandHandler( IAccountService accountService, IGrainFactory grainFactory, IBotService botService, IKaigenService kaigenService, IEvmManager evmManager) { _accountService = accountService; _grainFactory = grainFactory; _botService = botService; _kaigenService = kaigenService; _evmManager = evmManager; } public async Task Handle(StartCopyTradingCommand request, CancellationToken cancellationToken) { // Validate the trading balance if (request.BotTradingBalance <= Constants.GMX.Config.MinimumPositionAmount) { throw new ArgumentException( $"Bot trading balance must be greater than {Constants.GMX.Config.MinimumPositionAmount}"); } // Get the master bot configuration var masterBot = await _botService.GetBotByIdentifier(request.MasterBotIdentifier); if (masterBot == null) { throw new ArgumentException($"Master bot with identifier {request.MasterBotIdentifier} not found"); } // Special validation for Kudai strategy - check staking requirements if (string.Equals(request.MasterBotIdentifier.ToString(), "Kudai", StringComparison.OrdinalIgnoreCase)) { await ValidateKudaiStakingRequirements(request.User); }else { // Verify the user owns the keys of the master strategy var ownedKeys = await _kaigenService.GetOwnedKeysAsync(request.User); var hasMasterStrategyKey = ownedKeys.Items.Any(key => string.Equals(key.AgentName, request.MasterBotIdentifier.ToString(), StringComparison.OrdinalIgnoreCase) && key.Owned >= 1); if (!hasMasterStrategyKey) { throw new UnauthorizedAccessException( $"You don't own the keys for the master strategy '{request.MasterBotIdentifier}'. " + "You must own at least 1 key for this strategy to copy trade from it."); } } // Get the master bot configuration var masterConfig = await _botService.GetBotConfig(request.MasterBotIdentifier); if (masterConfig == null) { throw new InvalidOperationException($"Could not retrieve configuration for master bot {request.MasterBotIdentifier}"); } // Check if user already has a bot on this ticker (same as master bot) var hasExistingBotOnTicker = await _botService.HasUserBotOnTickerAsync(request.User.Id, masterConfig.Ticker); if (hasExistingBotOnTicker) { throw new InvalidOperationException( $"You already have a strategy running or saved on ticker {masterConfig.Ticker}. " + "You cannot create multiple strategies on the same ticker."); } // Get account information from the requesting user's accounts var userAccounts = await _accountService.GetAccountsByUserAsync(request.User, true, true); var firstAccount = userAccounts.FirstOrDefault(); if (firstAccount == null) { throw new InvalidOperationException($"User '{request.User.Name}' has no accounts configured."); } Account account = firstAccount; // Check balances for EVM/GMX V2 accounts before starting if (account.Exchange == TradingExchanges.Evm || account.Exchange == TradingExchanges.GmxV2) { var balanceCheckResult = await _botService.CheckAccountBalancesAsync(account); if (!balanceCheckResult.IsSuccessful) { throw new InvalidOperationException(balanceCheckResult.Message); } } Balance usdcBalance = null; // For other exchanges, keep the original USDC balance check if (account.Exchange != TradingExchanges.Evm && account.Exchange != TradingExchanges.GmxV2) { usdcBalance = account.Balances.FirstOrDefault(b => b.TokenName == Ticker.USDC.ToString()); if (usdcBalance == null || usdcBalance.Value < Constants.GMX.Config.MinimumPositionAmount || usdcBalance.Value < request.BotTradingBalance) { throw new Exception( $"Account {masterConfig.AccountName} has no USDC balance or not enough balance for copy trading"); } } // Enforce allocation limit across all bots using the same account var availableAllocation = await _botService.GetAvailableAllocationUsdAsync(account, default); if (request.BotTradingBalance > availableAllocation) { throw new InvalidOperationException( $"Insufficient available allocation. Requested: {request.BotTradingBalance:F2} USDC, " + $"Balance : {usdcBalance?.Value:F2 ?? 0} Available: {availableAllocation:F2} USDC."); } // Create copy trading configuration based on master bot var copyTradingConfig = new TradingBotConfig { // Copy all master configuration AccountName = account.Name, MoneyManagement = masterConfig.MoneyManagement, Ticker = masterConfig.Ticker, Scenario = masterConfig.Scenario, ScenarioName = masterConfig.ScenarioName, Timeframe = masterConfig.Timeframe, IsForWatchingOnly = masterConfig.IsForWatchingOnly, BotTradingBalance = request.BotTradingBalance, // Use the provided trading balance CooldownPeriod = masterConfig.CooldownPeriod, MaxLossStreak = masterConfig.MaxLossStreak, MaxPositionTimeHours = masterConfig.MaxPositionTimeHours, FlipOnlyWhenInProfit = masterConfig.FlipOnlyWhenInProfit, CloseEarlyWhenProfitable = masterConfig.CloseEarlyWhenProfitable, UseSynthApi = masterConfig.UseSynthApi, UseForPositionSizing = masterConfig.UseForPositionSizing, UseForSignalFiltering = masterConfig.UseForSignalFiltering, UseForDynamicStopLoss = masterConfig.UseForDynamicStopLoss, FlipPosition = masterConfig.FlipPosition, // Set copy trading specific properties IsForCopyTrading = true, MasterBotIdentifier = request.MasterBotIdentifier, // Set computed/default properties IsForBacktest = false, Name = masterConfig.Name }; try { var botGrain = _grainFactory.GetGrain(Guid.NewGuid()); await botGrain.CreateAsync(copyTradingConfig, request.User); // Start the copy trading bot immediately await botGrain.StartAsync(); return $"Copy trading bot started successfully, following master bot '{masterConfig.Name}' with {request.BotTradingBalance:F2} USDC balance"; } catch (Exception ex) { throw new Exception($"Failed to start copy trading bot: {ex.Message}, {ex.StackTrace}"); } } private async Task ValidateKudaiStakingRequirements(User user) { // Use the user's wallet address directly if (string.IsNullOrEmpty(user.OwnerWalletAddress)) { throw new InvalidOperationException( "To copy trade the Kudai strategy, you must have a wallet address configured in your profile."); } // Check KUDAI staked balance var kudaiStakedBalance = await _evmManager.GetKudaiStakedBalance(user.OwnerWalletAddress); // Check GBC staked NFT count var gbcStakedCount = await _evmManager.GetGbcStakedCount(user.OwnerWalletAddress); // Requirements: 100 million KUDAI OR 10 GBC NFTs const decimal requiredKudaiAmount = 100_000_000m; // 100 million const int requiredGbcCount = 10; if (kudaiStakedBalance < requiredKudaiAmount && gbcStakedCount < requiredGbcCount) { throw new UnauthorizedAccessException( $"To copy trade the Kudai strategy, you must have either:\n" + $"• {requiredKudaiAmount:N0} KUDAI tokens staked (currently have {kudaiStakedBalance:N0})\n" + $"• OR {requiredGbcCount} GBC NFTs staked (currently have {gbcStakedCount})\n\n" + $"Please stake the required amount in the Kudai staking contract to copy this strategy."); } } } }