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