215 lines
8.0 KiB
C#
215 lines
8.0 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Configuration settings for Kaigen credit management service.
|
|
/// The PrivateKey should be set via the KAIGEN_PRIVATE_KEY environment variable for security.
|
|
/// </summary>
|
|
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<KaigenService> _logger;
|
|
private readonly JsonSerializerOptions _jsonOptions;
|
|
private readonly bool _creditsEnabled;
|
|
|
|
public KaigenService(IOptions<KaigenSettings> options, ILogger<KaigenService> 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<string> 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<KaigenResponse>(_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<bool> 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; }
|
|
}
|
|
} |