Add copy trading authorization checks in LiveTradingBotGrain and StartCopyTradingCommandHandler. Integrated IKaigenService to verify user ownership of master strategy keys before allowing copy trading. Enhanced error handling and logging for authorization verification.
This commit is contained in:
@@ -19,4 +19,33 @@ public interface IKaigenService
|
|||||||
/// <param name="user">The user to refund</param>
|
/// <param name="user">The user to refund</param>
|
||||||
/// <returns>True if refund was successful</returns>
|
/// <returns>True if refund was successful</returns>
|
||||||
Task<bool> RefundUserCreditsAsync(string requestId, User user);
|
Task<bool> RefundUserCreditsAsync(string requestId, User user);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the owned keys for a user
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">The user to get owned keys for</param>
|
||||||
|
/// <returns>The owned keys response containing items and overall total</returns>
|
||||||
|
Task<OwnedKeysResponse> GetOwnedKeysAsync(User user);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Response DTO for owned keys API
|
||||||
|
/// </summary>
|
||||||
|
public class OwnedKeysResponse
|
||||||
|
{
|
||||||
|
public List<OwnedKeyItem> Items { get; set; }
|
||||||
|
public decimal Overall { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Individual owned key item DTO
|
||||||
|
/// </summary>
|
||||||
|
public class OwnedKeyItem
|
||||||
|
{
|
||||||
|
public string AgentName { get; set; }
|
||||||
|
public string WalletAddress { get; set; }
|
||||||
|
public decimal Owned { get; set; }
|
||||||
|
public string UnitPrice { get; set; }
|
||||||
|
public decimal Change24h { get; set; }
|
||||||
|
public decimal Total { get; set; }
|
||||||
}
|
}
|
||||||
@@ -29,6 +29,7 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable
|
|||||||
private readonly IPersistentState<TradingBotGrainState> _state;
|
private readonly IPersistentState<TradingBotGrainState> _state;
|
||||||
private readonly ILogger<LiveTradingBotGrain> _logger;
|
private readonly ILogger<LiveTradingBotGrain> _logger;
|
||||||
private readonly IServiceScopeFactory _scopeFactory;
|
private readonly IServiceScopeFactory _scopeFactory;
|
||||||
|
private readonly IKaigenService _kaigenService;
|
||||||
private TradingBotBase? _tradingBot;
|
private TradingBotBase? _tradingBot;
|
||||||
private IDisposable? _timer;
|
private IDisposable? _timer;
|
||||||
private string _reminderName = "RebootReminder";
|
private string _reminderName = "RebootReminder";
|
||||||
@@ -38,11 +39,13 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable
|
|||||||
[PersistentState("live-trading-bot", "bot-store")]
|
[PersistentState("live-trading-bot", "bot-store")]
|
||||||
IPersistentState<TradingBotGrainState> state,
|
IPersistentState<TradingBotGrainState> state,
|
||||||
ILogger<LiveTradingBotGrain> logger,
|
ILogger<LiveTradingBotGrain> logger,
|
||||||
IServiceScopeFactory scopeFactory)
|
IServiceScopeFactory scopeFactory,
|
||||||
|
IKaigenService kaigenService)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_scopeFactory = scopeFactory;
|
_scopeFactory = scopeFactory;
|
||||||
_state = state;
|
_state = state;
|
||||||
|
_kaigenService = kaigenService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task OnActivateAsync(CancellationToken cancellationToken)
|
public override async Task OnActivateAsync(CancellationToken cancellationToken)
|
||||||
@@ -505,6 +508,35 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if copy trading authorization is still valid
|
||||||
|
if (_state.State.Config.IsForCopyTrading && _state.State.Config.MasterBotIdentifier.HasValue)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var ownedKeys = await _kaigenService.GetOwnedKeysAsync(_state.State.User);
|
||||||
|
var hasMasterStrategyKey = ownedKeys.Items.Any(key =>
|
||||||
|
string.Equals(key.AgentName, _state.State.Config.MasterBotIdentifier.Value.ToString(), StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
key.Owned >= 1);
|
||||||
|
|
||||||
|
if (!hasMasterStrategyKey)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
"Copy trading bot {GrainId} no longer has authorization for master strategy {MasterBotId}. Stopping bot.",
|
||||||
|
this.GetPrimaryKey(), _state.State.Config.MasterBotIdentifier.Value);
|
||||||
|
|
||||||
|
await StopAsync("Copy trading authorization revoked - user no longer owns keys for master strategy");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex,
|
||||||
|
"Failed to verify copy trading authorization for bot {GrainId} with master strategy {MasterBotId}. Continuing execution.",
|
||||||
|
this.GetPrimaryKey(), _state.State.Config.MasterBotIdentifier.Value);
|
||||||
|
SentrySdk.CaptureException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (_tradingBot.Positions.Any(p => p.Value.IsOpen() || p.Value.Status.Equals(PositionStatus.New)))
|
if (_tradingBot.Positions.Any(p => p.Value.IsOpen() || p.Value.Status.Equals(PositionStatus.New)))
|
||||||
{
|
{
|
||||||
_logger.LogInformation(
|
_logger.LogInformation(
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using Managing.Domain.Accounts;
|
|||||||
using Managing.Domain.Bots;
|
using Managing.Domain.Bots;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using static Managing.Common.Enums;
|
using static Managing.Common.Enums;
|
||||||
|
using System;
|
||||||
|
|
||||||
namespace Managing.Application.ManageBot
|
namespace Managing.Application.ManageBot
|
||||||
{
|
{
|
||||||
@@ -15,13 +16,15 @@ namespace Managing.Application.ManageBot
|
|||||||
private readonly IAccountService _accountService;
|
private readonly IAccountService _accountService;
|
||||||
private readonly IGrainFactory _grainFactory;
|
private readonly IGrainFactory _grainFactory;
|
||||||
private readonly IBotService _botService;
|
private readonly IBotService _botService;
|
||||||
|
private readonly IKaigenService _kaigenService;
|
||||||
|
|
||||||
public StartCopyTradingCommandHandler(
|
public StartCopyTradingCommandHandler(
|
||||||
IAccountService accountService, IGrainFactory grainFactory, IBotService botService)
|
IAccountService accountService, IGrainFactory grainFactory, IBotService botService, IKaigenService kaigenService)
|
||||||
{
|
{
|
||||||
_accountService = accountService;
|
_accountService = accountService;
|
||||||
_grainFactory = grainFactory;
|
_grainFactory = grainFactory;
|
||||||
_botService = botService;
|
_botService = botService;
|
||||||
|
_kaigenService = kaigenService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string> Handle(StartCopyTradingCommand request, CancellationToken cancellationToken)
|
public async Task<string> Handle(StartCopyTradingCommand request, CancellationToken cancellationToken)
|
||||||
@@ -40,11 +43,18 @@ namespace Managing.Application.ManageBot
|
|||||||
throw new ArgumentException($"Master bot with identifier {request.MasterBotIdentifier} not found");
|
throw new ArgumentException($"Master bot with identifier {request.MasterBotIdentifier} not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify the user owns the master bot keys
|
// Verify the user owns the keys of the master strategy
|
||||||
// if (masterBot.User?.Name != request.User.Name)
|
var ownedKeys = await _kaigenService.GetOwnedKeysAsync(request.User);
|
||||||
// {
|
var hasMasterStrategyKey = ownedKeys.Items.Any(key =>
|
||||||
// throw new UnauthorizedAccessException("You don't have permission to copy trades from this bot");
|
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
|
// Get the master bot configuration
|
||||||
var masterConfig = await _botService.GetBotConfig(request.MasterBotIdentifier);
|
var masterConfig = await _botService.GetBotConfig(request.MasterBotIdentifier);
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ public class KaigenSettings
|
|||||||
public string BaseUrl { get; set; } = "https://api.kaigen.managing.live";
|
public string BaseUrl { get; set; } = "https://api.kaigen.managing.live";
|
||||||
public string DebitEndpoint { get; set; } = "/api/credits/debit";
|
public string DebitEndpoint { get; set; } = "/api/credits/debit";
|
||||||
public string RefundEndpoint { get; set; } = "/api/credits/refund";
|
public string RefundEndpoint { get; set; } = "/api/credits/refund";
|
||||||
|
public string OwnedKeysEndpoint { get; set; } = "/api/keys/owned";
|
||||||
public string SecretKey { get; set; } = string.Empty;
|
public string SecretKey { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,24 +170,48 @@ public class KaigenService : IKaigenService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<OwnedKeysResponse> GetOwnedKeysAsync(User user)
|
||||||
|
{
|
||||||
|
// If credits are disabled, return empty response
|
||||||
|
if (!_creditsEnabled)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Credits disabled - returning empty owned keys for user {UserName}",
|
||||||
|
user.Name);
|
||||||
|
return new OwnedKeysResponse { Items = new List<OwnedKeyItem>(), Overall = 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var walletAddress = GetUserWalletAddress(user);
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Fetching owned keys for user {UserName} (wallet: {WalletAddress})",
|
||||||
|
user.Name, walletAddress);
|
||||||
|
|
||||||
|
var result = await SendAuthenticatedGetRequestAsync(
|
||||||
|
$"{_settings.BaseUrl}{_settings.OwnedKeysEndpoint}",
|
||||||
|
user);
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Successfully fetched owned keys for user {UserName} (wallet: {WalletAddress})",
|
||||||
|
user.Name, walletAddress);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error fetching owned keys for user {UserName}", user.Name);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<KaigenResponse> SendAuthenticatedRequestAsync(string url, object payload, User user)
|
private async Task<KaigenResponse> SendAuthenticatedRequestAsync(string url, object payload, User user)
|
||||||
{
|
{
|
||||||
// Create the auth token: "walletaddress-username"
|
|
||||||
var authToken = $"{GetUserWalletAddress(user)}-{user.Name}";
|
|
||||||
|
|
||||||
// Encrypt the auth token using AES-256-GCM
|
|
||||||
var encryptedToken = CryptoHelpers.EncryptAesGcm(authToken, _settings.SecretKey);
|
|
||||||
|
|
||||||
// Create Basic Auth header with the encrypted token
|
|
||||||
var basicAuthString = $"{encryptedToken}:";
|
|
||||||
var base64Auth = Convert.ToBase64String(Encoding.ASCII.GetBytes(basicAuthString));
|
|
||||||
|
|
||||||
// Create a new request with the auth header
|
|
||||||
var request = new HttpRequestMessage(HttpMethod.Put, url)
|
var request = new HttpRequestMessage(HttpMethod.Put, url)
|
||||||
{
|
{
|
||||||
Content = JsonContent.Create(payload, options: _jsonOptions)
|
Content = JsonContent.Create(payload, options: _jsonOptions)
|
||||||
};
|
};
|
||||||
request.Headers.Authorization = new AuthenticationHeaderValue("Basic", base64Auth);
|
request.Headers.Authorization = BuildAuthorizationHeader(user);
|
||||||
|
|
||||||
var response = await _httpClient.SendAsync(request);
|
var response = await _httpClient.SendAsync(request);
|
||||||
|
|
||||||
@@ -202,6 +227,40 @@ public class KaigenService : IKaigenService
|
|||||||
return result ?? new KaigenResponse { Success = false, Message = "Failed to parse response" };
|
return result ?? new KaigenResponse { Success = false, Message = "Failed to parse response" };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<OwnedKeysResponse> SendAuthenticatedGetRequestAsync(string url, User user)
|
||||||
|
{
|
||||||
|
var request = new HttpRequestMessage(HttpMethod.Get, url);
|
||||||
|
request.Headers.Authorization = BuildAuthorizationHeader(user);
|
||||||
|
|
||||||
|
var response = await _httpClient.SendAsync(request);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var errorContent = await response.Content.ReadAsStringAsync();
|
||||||
|
_logger.LogError("GET request failed. Status: {StatusCode}, Error: {Error}",
|
||||||
|
response.StatusCode, errorContent);
|
||||||
|
throw new Exception($"GET request failed: HTTP {response.StatusCode}: {errorContent}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await response.Content.ReadFromJsonAsync<OwnedKeysResponse>(_jsonOptions);
|
||||||
|
return result ?? new OwnedKeysResponse { Items = new List<OwnedKeyItem>(), Overall = 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
private AuthenticationHeaderValue BuildAuthorizationHeader(User user)
|
||||||
|
{
|
||||||
|
// Create the auth token: "walletaddress-username"
|
||||||
|
var authToken = $"{GetUserWalletAddress(user)}-{user.Name}";
|
||||||
|
|
||||||
|
// Encrypt the auth token using AES-256-GCM
|
||||||
|
var encryptedToken = CryptoHelpers.EncryptAesGcm(authToken, _settings.SecretKey);
|
||||||
|
|
||||||
|
// Create Basic Auth header with the encrypted token
|
||||||
|
var basicAuthString = $"{encryptedToken}:";
|
||||||
|
var base64Auth = Convert.ToBase64String(Encoding.ASCII.GetBytes(basicAuthString));
|
||||||
|
|
||||||
|
return new AuthenticationHeaderValue("Basic", base64Auth);
|
||||||
|
}
|
||||||
|
|
||||||
private string GetUserWalletAddress(User user)
|
private string GetUserWalletAddress(User user)
|
||||||
{
|
{
|
||||||
if (user?.Accounts == null || !user.Accounts.Any())
|
if (user?.Accounts == null || !user.Accounts.Any())
|
||||||
|
|||||||
Reference in New Issue
Block a user