using System.Net.Http.Json; using System.Text.Json; using Managing.Application.Abstractions.Services; using Managing.Domain.Users; 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; private readonly bool _creditsEnabled; public KaigenService(IOptions options, ILogger logger) { _httpClient = new HttpClient(); _settings = options.Value; _logger = logger; _jsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; // Check if credits are enabled via environment variable var creditsEnabledEnv = Environment.GetEnvironmentVariable("KAIGEN_CREDITS_ENABLED"); _creditsEnabled = string.IsNullOrEmpty(creditsEnabledEnv) || creditsEnabledEnv.ToLower() == "true"; if (!_creditsEnabled) { _logger.LogInformation("Kaigen credits are disabled via KAIGEN_CREDITS_ENABLED environment variable"); return; // Skip private key validation if credits are disabled } // Always read from environment variable for security var envPrivateKey = Environment.GetEnvironmentVariable("KAIGEN_PRIVATE_KEY"); if (!string.IsNullOrEmpty(envPrivateKey)) { _settings.PrivateKey = envPrivateKey; _logger.LogInformation("Using KAIGEN_PRIVATE_KEY from environment variable"); } // Validate required settings only if credits are enabled 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(User user, decimal debitAmount) { // If credits are disabled, return a dummy request ID if (!_creditsEnabled) { var dummyRequestId = Guid.NewGuid().ToString(); _logger.LogInformation("Credits disabled - skipping debit for user {UserName}. Returning dummy request ID {RequestId}", user.Name, dummyRequestId); return dummyRequestId; } try { var walletAddress = GetUserWalletAddress(user); var requestId = Guid.NewGuid().ToString(); // Create the message to sign (concatenate the values) var message = $"{requestId}{walletAddress}{debitAmount}"; // Sign the message var signature = SignMessage(message, _settings.PrivateKey); // Create the request payload var requestPayload = new { requestId = requestId, walletAddress = walletAddress, debitAmount = debitAmount, signature = signature }; _logger.LogInformation("Debiting {Amount} credits for user {UserName} (wallet: {WalletAddress}) with request ID {RequestId}", debitAmount, user.Name, walletAddress, 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} (wallet: {WalletAddress})", debitAmount, user.Name, walletAddress); return requestId; } catch (Exception ex) { _logger.LogError(ex, "Error debiting credits for user {UserName}", user.Name); throw; } } public async Task RefundUserCreditsAsync(string requestId, User user) { // If credits are disabled, return true (success) immediately if (!_creditsEnabled) { _logger.LogInformation("Credits disabled - skipping refund for user {UserName} with request ID {RequestId}", user.Name, requestId); return true; } try { var walletAddress = GetUserWalletAddress(user); // Create the message to sign (concatenate the values) var message = $"{requestId}{walletAddress}"; // Sign the message var signature = SignMessage(message, _settings.PrivateKey); // Create the request payload var requestPayload = new { requestId = requestId, walletAddress = walletAddress, signature = signature }; _logger.LogInformation("Refunding credits for user {UserName} (wallet: {WalletAddress}) with request ID {RequestId}", user.Name, walletAddress, 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} (wallet: {WalletAddress})", user.Name, walletAddress); return true; } catch (Exception ex) { _logger.LogError(ex, "Error refunding credits for user {UserName}", user.Name); return false; } } private string GetUserWalletAddress(User user) { if (user?.Accounts == null || !user.Accounts.Any()) { throw new InvalidOperationException($"No accounts found for user {user?.Name}"); } // Use the first account's key as the wallet address var walletAddress = user.Accounts[0].Key; if (string.IsNullOrEmpty(walletAddress)) { throw new InvalidOperationException($"No wallet address found for user {user.Name}"); } return walletAddress; } 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; } } }