From 11778aa2a47232eb640d72fdb6b14b2126b2dde3 Mon Sep 17 00:00:00 2001 From: cryptooda Date: Tue, 15 Jul 2025 10:31:21 +0700 Subject: [PATCH] Add kaigen debit credit for backtest --- docs/KaigenConfiguration.md | 64 +++++++ src/Managing.Api/appsettings.Production.json | 8 +- src/Managing.Api/appsettings.json | 6 + .../Services/IKaigenService.cs | 20 +++ .../Backtesting/Backtester.cs | 73 ++++++-- src/Managing.Bootstrap/ApiBootstrap.cs | 3 + .../Services/KaigenService.cs | 165 ++++++++++++++++++ 7 files changed, 326 insertions(+), 13 deletions(-) create mode 100644 docs/KaigenConfiguration.md create mode 100644 src/Managing.Application.Abstractions/Services/IKaigenService.cs create mode 100644 src/Managing.Infrastructure.Web3/Services/KaigenService.cs diff --git a/docs/KaigenConfiguration.md b/docs/KaigenConfiguration.md new file mode 100644 index 0000000..dac1429 --- /dev/null +++ b/docs/KaigenConfiguration.md @@ -0,0 +1,64 @@ +# Kaigen Service Configuration + +The Kaigen service is used for managing user credits during backtest operations. It requires proper configuration to function correctly. + +## Environment Variables + +### Required Environment Variable + +- **`KAIGEN_PRIVATE_KEY`**: The private key used for signing API requests to the Kaigen service. + +### Setting the Environment Variable + +#### Development +```bash +export KAIGEN_PRIVATE_KEY="your-private-key-here" +``` + +#### Production +Set the environment variable in your deployment configuration: +```bash +KAIGEN_PRIVATE_KEY=your-private-key-here +``` + +#### Docker +```bash +docker run -e KAIGEN_PRIVATE_KEY=your-private-key-here your-app +``` + +#### Docker Compose +```yaml +environment: + - KAIGEN_PRIVATE_KEY=your-private-key-here +``` + +## Configuration Structure + +The Kaigen service configuration is defined in `appsettings.json`: + +```json +{ + "Kaigen": { + "BaseUrl": "https://api.kaigen.managing.live", + "DebitEndpoint": "/api/credits/debit", + "RefundEndpoint": "/api/credits/refund", + "PrivateKey": "${KAIGEN_PRIVATE_KEY}" + } +} +``` + +## API Endpoints + +- **PUT** `/api/credits/debit` - Debit credits from user account +- **PUT** `/api/credits/refund` - Refund credits to user account + +## Security Notes + +- The private key should never be committed to source control +- Use environment variables or secure configuration management systems +- The private key is used for signing API requests to ensure authenticity +- Rotate the private key regularly for enhanced security + +## Error Handling + +If the `KAIGEN_PRIVATE_KEY` environment variable is not set, the application will throw an `InvalidOperationException` with a clear error message during startup. \ No newline at end of file diff --git a/src/Managing.Api/appsettings.Production.json b/src/Managing.Api/appsettings.Production.json index a968b63..e9f9567 100644 --- a/src/Managing.Api/appsettings.Production.json +++ b/src/Managing.Api/appsettings.Production.json @@ -32,5 +32,11 @@ }, "AllowedHosts": "*", "WorkerBotManager": true, - "WorkerBalancesTracking": true + "WorkerBalancesTracking": true, + "Kaigen": { + "BaseUrl": "https://api.kaigen.managing.live", + "DebitEndpoint": "/api/credits/debit", + "RefundEndpoint": "/api/credits/refund", + "PrivateKey": "${KAIGEN_PRIVATE_KEY}" + } } \ No newline at end of file diff --git a/src/Managing.Api/appsettings.json b/src/Managing.Api/appsettings.json index 66ea9e6..41dfa96 100644 --- a/src/Managing.Api/appsettings.json +++ b/src/Managing.Api/appsettings.json @@ -24,6 +24,12 @@ "Web3Proxy": { "BaseUrl": "http://localhost:4111" }, + "Kaigen": { + "BaseUrl": "https://api.kaigen.managing.live", + "DebitEndpoint": "/api/credits/debit", + "RefundEndpoint": "/api/credits/refund", + "PrivateKey": "${KAIGEN_PRIVATE_KEY}" + }, "N8n": { "WebhookUrl": "https://n8n.kai.managing.live/webhook/fa9308b6-983b-42ec-b085-71599d655951" }, diff --git a/src/Managing.Application.Abstractions/Services/IKaigenService.cs b/src/Managing.Application.Abstractions/Services/IKaigenService.cs new file mode 100644 index 0000000..07ce632 --- /dev/null +++ b/src/Managing.Application.Abstractions/Services/IKaigenService.cs @@ -0,0 +1,20 @@ +namespace Managing.Application.Abstractions.Services; + +public interface IKaigenService +{ + /// + /// Debits user credits for a backtest operation + /// + /// The username to debit + /// The amount to debit + /// The request ID for tracking + Task DebitUserCreditsAsync(string userName, decimal debitAmount); + + /// + /// Refunds user credits if debit operation fails + /// + /// The original request ID from debit operation + /// The username to refund + /// True if refund was successful + Task RefundUserCreditsAsync(string requestId, string userName); +} \ No newline at end of file diff --git a/src/Managing.Application/Backtesting/Backtester.cs b/src/Managing.Application/Backtesting/Backtester.cs index 9cfbebd..6e77b20 100644 --- a/src/Managing.Application/Backtesting/Backtester.cs +++ b/src/Managing.Application/Backtesting/Backtester.cs @@ -27,6 +27,7 @@ namespace Managing.Application.Backtesting private readonly IScenarioService _scenarioService; private readonly IAccountService _accountService; private readonly IMessengerService _messengerService; + private readonly IKaigenService _kaigenService; public Backtester( IExchangeService exchangeService, @@ -35,7 +36,8 @@ namespace Managing.Application.Backtesting ILogger logger, IScenarioService scenarioService, IAccountService accountService, - IMessengerService messengerService) + IMessengerService messengerService, + IKaigenService kaigenService) { _exchangeService = exchangeService; _botFactory = botFactory; @@ -44,6 +46,7 @@ namespace Managing.Application.Backtesting _scenarioService = scenarioService; _accountService = accountService; _messengerService = messengerService; + _kaigenService = kaigenService; } public Backtest RunSimpleBotBacktest(Workflow workflow, bool save = false) @@ -82,20 +85,66 @@ namespace Managing.Application.Backtesting string requestId = null, object metadata = null) { - var candles = GetCandles(config.Ticker, config.Timeframe, startDate, endDate); - - var result = await RunBacktestWithCandles(config, candles, user, withCandles, requestId, metadata); - - // Set start and end dates - result.StartDate = startDate; - result.EndDate = endDate; - - if (save && user != null) + string creditRequestId = null; + + // Debit user credits before starting the backtest + if (user != null) { - _backtestRepository.InsertBacktestForUser(user, result); + try + { + creditRequestId = await _kaigenService.DebitUserCreditsAsync(user.Name, 3); + _logger.LogInformation("Successfully debited credits for user {UserName} with request ID {RequestId}", + user.Name, creditRequestId); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to debit credits for user {UserName}. Backtest will not proceed.", user.Name); + throw new Exception($"Failed to debit credits: {ex.Message}"); + } } - return result; + try + { + var candles = GetCandles(config.Ticker, config.Timeframe, startDate, endDate); + + var result = await RunBacktestWithCandles(config, candles, user, withCandles, requestId, metadata); + + // Set start and end dates + result.StartDate = startDate; + result.EndDate = endDate; + + if (save && user != null) + { + _backtestRepository.InsertBacktestForUser(user, result); + } + + return result; + } + catch (Exception ex) + { + // If backtest fails and we debited credits, attempt to refund + if (user != null && !string.IsNullOrEmpty(creditRequestId)) + { + try + { + var refundSuccess = await _kaigenService.RefundUserCreditsAsync(creditRequestId, user.Name); + if (refundSuccess) + { + _logger.LogInformation("Successfully refunded credits for user {UserName} after backtest failure", user.Name); + } + else + { + _logger.LogError("Failed to refund credits for user {UserName} after backtest failure", user.Name); + } + } + catch (Exception refundEx) + { + _logger.LogError(refundEx, "Error during refund attempt for user {UserName}", user.Name); + } + } + + throw; + } } /// diff --git a/src/Managing.Bootstrap/ApiBootstrap.cs b/src/Managing.Bootstrap/ApiBootstrap.cs index b76b2c0..96a6330 100644 --- a/src/Managing.Bootstrap/ApiBootstrap.cs +++ b/src/Managing.Bootstrap/ApiBootstrap.cs @@ -100,6 +100,7 @@ public static class ApiBootstrap services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); return services; } @@ -118,6 +119,8 @@ public static class ApiBootstrap services.AddSingleton(sp => sp.GetRequiredService>().Value); + services.Configure(configuration.GetSection("Kaigen")); + // Evm services.AddGbcFeed(); services.AddUniswapV2(); diff --git a/src/Managing.Infrastructure.Web3/Services/KaigenService.cs b/src/Managing.Infrastructure.Web3/Services/KaigenService.cs new file mode 100644 index 0000000..949ff3f --- /dev/null +++ b/src/Managing.Infrastructure.Web3/Services/KaigenService.cs @@ -0,0 +1,165 @@ +using System.Net.Http.Json; +using System.Text.Json; +using Managing.Application.Abstractions.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Nethereum.Signer; + +namespace Managing.Infrastructure.Evm.Services; + +/// +/// Configuration settings for Kaigen credit management service. +/// The PrivateKey should be set via the KAIGEN_PRIVATE_KEY environment variable for security. +/// +public class KaigenSettings +{ + public string BaseUrl { get; set; } = "https://api.kaigen.managing.live"; + public string DebitEndpoint { get; set; } = "/api/credits/debit"; + public string RefundEndpoint { get; set; } = "/api/credits/refund"; + public string PrivateKey { get; set; } = string.Empty; +} + +public class KaigenService : IKaigenService +{ + private readonly HttpClient _httpClient; + private readonly KaigenSettings _settings; + private readonly ILogger _logger; + private readonly JsonSerializerOptions _jsonOptions; + + public KaigenService(IOptions options, ILogger logger) + { + _httpClient = new HttpClient(); + _settings = options.Value; + _logger = logger; + _jsonOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + // Validate required settings + if (string.IsNullOrEmpty(_settings.PrivateKey)) + { + throw new InvalidOperationException("Kaigen PrivateKey is not configured. Please set the KAIGEN_PRIVATE_KEY environment variable."); + } + + if (string.IsNullOrEmpty(_settings.BaseUrl)) + { + throw new InvalidOperationException("Kaigen BaseUrl is not configured."); + } + } + + public async Task DebitUserCreditsAsync(string userName, decimal debitAmount) + { + try + { + var requestId = Guid.NewGuid().ToString(); + + // Create the payload for signing + var payload = new + { + RequestId = requestId, + UserName = userName, + DebitAmount = debitAmount + }; + + // Create the message to sign (concatenate the values) + var message = $"{requestId}{userName}{debitAmount}"; + + // Sign the message + var signature = SignMessage(message, _settings.PrivateKey); + + // Create the request payload + var requestPayload = new + { + requestId = requestId, + userName = userName, + debitAmount = debitAmount, + signature = signature + }; + + _logger.LogInformation("Debiting {Amount} credits for user {UserName} with request ID {RequestId}", + debitAmount, userName, requestId); + + var response = await _httpClient.PutAsJsonAsync( + $"{_settings.BaseUrl}{_settings.DebitEndpoint}", + requestPayload, + _jsonOptions); + + if (!response.IsSuccessStatusCode) + { + var errorContent = await response.Content.ReadAsStringAsync(); + _logger.LogError("Failed to debit credits. Status: {StatusCode}, Error: {Error}", + response.StatusCode, errorContent); + throw new Exception($"Failed to debit credits: {response.StatusCode} - {errorContent}"); + } + + var result = await response.Content.ReadFromJsonAsync(_jsonOptions); + _logger.LogInformation("Successfully debited {Amount} credits for user {UserName}", + debitAmount, userName); + + return requestId; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error debiting credits for user {UserName}", userName); + throw; + } + } + + public async Task RefundUserCreditsAsync(string requestId, string userName) + { + try + { + // Create the message to sign (concatenate the values) + var message = $"{requestId}{userName}"; + + // Sign the message + var signature = SignMessage(message, _settings.PrivateKey); + + // Create the request payload + var requestPayload = new + { + requestId = requestId, + userName = userName, + signature = signature + }; + + _logger.LogInformation("Refunding credits for user {UserName} with request ID {RequestId}", + userName, requestId); + + var response = await _httpClient.PutAsJsonAsync( + $"{_settings.BaseUrl}{_settings.RefundEndpoint}", + requestPayload, + _jsonOptions); + + if (!response.IsSuccessStatusCode) + { + var errorContent = await response.Content.ReadAsStringAsync(); + _logger.LogError("Failed to refund credits. Status: {StatusCode}, Error: {Error}", + response.StatusCode, errorContent); + return false; + } + + _logger.LogInformation("Successfully refunded credits for user {UserName}", userName); + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error refunding credits for user {UserName}", userName); + return false; + } + } + + private string SignMessage(string message, string privateKey) + { + var signer = new EthereumMessageSigner(); + var signature = signer.EncodeUTF8AndSign(message, new EthECKey(privateKey)); + return signature; + } + + private class KaigenResponse + { + public bool Success { get; set; } + public string Message { get; set; } + } +} \ No newline at end of file