using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using LittleShop.Enums; namespace LittleShop.Services; public class SilverPayService : ISilverPayService { private readonly HttpClient _httpClient; private readonly IConfiguration _configuration; private readonly ILogger _logger; private readonly IServiceProvider _serviceProvider; public SilverPayService( HttpClient httpClient, IConfiguration configuration, IServiceProvider serviceProvider, ILogger logger) { _httpClient = httpClient; _configuration = configuration; _serviceProvider = serviceProvider; _logger = logger; // Note: We'll initialize the HTTP client dynamically when needed // to always use the latest settings from the database _httpClient.DefaultRequestHeaders.Add("Accept", "application/json"); } private async Task<(string baseUrl, string apiKey, string webhookSecret)> GetSettingsAsync() { // Create a new scope to get the settings service using var scope = _serviceProvider.CreateScope(); var settingsService = scope.ServiceProvider.GetRequiredService(); // First try to get settings from the database var baseUrl = await settingsService.GetSettingAsync("SilverPay.BaseUrl"); var apiKey = await settingsService.GetSettingAsync("SilverPay.ApiKey"); var webhookSecret = await settingsService.GetSettingAsync("SilverPay.WebhookSecret"); // Fall back to configuration file if not set in database if (string.IsNullOrEmpty(baseUrl)) baseUrl = _configuration["SilverPay:BaseUrl"]; if (string.IsNullOrEmpty(apiKey)) apiKey = _configuration["SilverPay:ApiKey"]; if (string.IsNullOrEmpty(webhookSecret)) webhookSecret = _configuration["SilverPay:WebhookSecret"]; // Validate that we have at least a base URL if (string.IsNullOrEmpty(baseUrl)) throw new InvalidOperationException("SilverPay base URL not configured. Please configure it in the System Settings."); return (baseUrl!, apiKey ?? "", webhookSecret ?? ""); } private async Task ConfigureHttpClientAsync() { var (baseUrl, apiKey, _) = await GetSettingsAsync(); // Update base address if it has changed if (_httpClient.BaseAddress?.ToString() != baseUrl) { _httpClient.BaseAddress = new Uri(baseUrl); _logger.LogInformation("Updated SilverPay base URL to {BaseUrl}", baseUrl); } // Update API key header _httpClient.DefaultRequestHeaders.Remove("X-API-Key"); if (!string.IsNullOrEmpty(apiKey)) { _httpClient.DefaultRequestHeaders.Add("X-API-Key", apiKey); } } public async Task CreateOrderAsync( string externalId, decimal amount, CryptoCurrency currency, string? description = null, string? webhookUrl = null) { try { // Configure HTTP client with latest settings await ConfigureHttpClientAsync(); var currencyCode = GetSilverPayCurrency(currency); // Get settings for webhook URL using var scope = _serviceProvider.CreateScope(); var settingsService = scope.ServiceProvider.GetRequiredService(); var defaultWebhookUrl = await settingsService.GetSettingAsync("SilverPay.DefaultWebhookUrl") ?? _configuration["SilverPay:DefaultWebhookUrl"]; // Prepare request body for SilverPAY var request = new { external_id = externalId, amount = amount, // Amount in GBP fiat_currency = "GBP", currency = currencyCode, webhook_url = webhookUrl ?? defaultWebhookUrl, expires_in_hours = 24 }; var json = JsonSerializer.Serialize(request, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }); var content = new StringContent(json, Encoding.UTF8, "application/json"); _logger.LogInformation("Creating SilverPAY order - External ID: {ExternalId}, Amount: {Amount} GBP, Currency: {Currency}", externalId, amount, currencyCode); _logger.LogInformation("SilverPAY request body: {RequestBody}", json); // Add timeout to prevent hanging using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); var response = await _httpClient.PostAsync("/api/v1/orders", content, cts.Token); if (!response.IsSuccessStatusCode) { var errorContent = await response.Content.ReadAsStringAsync(); _logger.LogError("Failed to create SilverPAY order. Status: {Status}, Error: {Error}", response.StatusCode, errorContent); // Check if it's a server error that might warrant fallback if ((int)response.StatusCode >= 500) { throw new HttpRequestException($"SilverPAY server error: {response.StatusCode}", null, response.StatusCode); } throw new InvalidOperationException($"Failed to create SilverPAY order: {response.StatusCode}"); } var responseJson = await response.Content.ReadAsStringAsync(); var orderResponse = JsonSerializer.Deserialize(responseJson, new JsonSerializerOptions { PropertyNameCaseInsensitive = true, PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower }); if (orderResponse == null) { throw new InvalidOperationException("Invalid response from SilverPAY"); } _logger.LogInformation("✅ Created SilverPAY order {OrderId} for External ID {ExternalId} - Amount: {Amount} GBP, Currency: {Currency}, Address: {Address}", orderResponse.Id, externalId, amount, currencyCode, orderResponse.PaymentAddress); return MapToOrderResponse(orderResponse); } catch (HttpRequestException ex) { _logger.LogError(ex, "Network error creating SilverPAY order"); throw new InvalidOperationException("Network error contacting SilverPAY", ex); } catch (Exception ex) { _logger.LogError(ex, "❌ Failed to create SilverPAY order - External ID: {ExternalId}, Amount: {Amount} GBP", externalId, amount); throw; } } public async Task GetOrderStatusAsync(string orderId) { try { // Configure HTTP client with latest settings await ConfigureHttpClientAsync(); var response = await _httpClient.GetAsync($"/api/v1/orders/{orderId}"); if (response.StatusCode == System.Net.HttpStatusCode.NotFound) { return null; } if (!response.IsSuccessStatusCode) { _logger.LogWarning("Failed to get SilverPAY order status. Status: {Status}", response.StatusCode); return null; } var json = await response.Content.ReadAsStringAsync(); var orderResponse = JsonSerializer.Deserialize(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true, PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower }); return orderResponse != null ? MapToOrderResponse(orderResponse) : null; } catch (Exception ex) { _logger.LogError(ex, "Error getting SilverPAY order status for {OrderId}", orderId); return null; } } public async Task ValidateWebhookAsync(string payload, string signature) { try { // Get webhook secret from settings var (_, _, webhookSecret) = await GetSettingsAsync(); // SilverPAY webhook validation // The exact format depends on SilverPAY's implementation // This is a common HMAC-SHA256 validation pattern if (string.IsNullOrEmpty(webhookSecret)) { _logger.LogWarning("Webhook secret not configured, skipping validation"); return true; // Allow in development } var secretBytes = Encoding.UTF8.GetBytes(webhookSecret); var payloadBytes = Encoding.UTF8.GetBytes(payload); using var hmac = new System.Security.Cryptography.HMACSHA256(secretBytes); var computedHash = hmac.ComputeHash(payloadBytes); var computedHashHex = Convert.ToHexString(computedHash).ToLowerInvariant(); // SilverPAY might use different signature formats // Adjust based on actual implementation var expectedHash = signature.Replace("sha256=", "").ToLowerInvariant(); return computedHashHex.Equals(expectedHash, StringComparison.OrdinalIgnoreCase); } catch (Exception ex) { _logger.LogError(ex, "Error validating webhook signature"); return false; } } public async Task GetExchangeRateAsync(string cryptoCurrency, string fiatCurrency = "GBP") { try { // Configure HTTP client with latest settings await ConfigureHttpClientAsync(); var response = await _httpClient.GetAsync($"/api/v1/exchange-rates?crypto={cryptoCurrency}&fiat={fiatCurrency}"); if (!response.IsSuccessStatusCode) { return null; } var json = await response.Content.ReadAsStringAsync(); var rateResponse = JsonSerializer.Deserialize(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); return rateResponse?.Rate; } catch (Exception ex) { _logger.LogError(ex, "Error getting exchange rate for {Crypto}/{Fiat}", cryptoCurrency, fiatCurrency); return null; } } public async Task> GetSupportedCurrenciesAsync() { try { // Configure HTTP client with latest settings await ConfigureHttpClientAsync(); var response = await _httpClient.GetAsync("/api/v1/currencies"); if (!response.IsSuccessStatusCode) { _logger.LogWarning("Failed to get supported currencies from SilverPAY. Status: {Status}", response.StatusCode); // Return a default list of commonly supported currencies return new List { "BTC", "ETH", "USDT", "LTC" }; } var json = await response.Content.ReadAsStringAsync(); var currencies = JsonSerializer.Deserialize>(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); return currencies ?? new List { "BTC" }; } catch (Exception ex) { _logger.LogError(ex, "Error getting supported currencies from SilverPAY"); // Return a safe default return new List { "BTC" }; } } private static string GetSilverPayCurrency(CryptoCurrency currency) { return currency switch { CryptoCurrency.BTC => "BTC", CryptoCurrency.XMR => "XMR", // Monero not directly supported, might need mapping CryptoCurrency.USDT => "USDT", // Might map to ETH for USDT on Ethereum CryptoCurrency.LTC => "LTC", CryptoCurrency.ETH => "ETH", CryptoCurrency.ZEC => "ZEC", // Zcash might not be supported CryptoCurrency.DASH => "DASH", // Dash might not be supported CryptoCurrency.DOGE => "DOGE", // Dogecoin might not be supported _ => "BTC" }; } private static SilverPayOrderResponse MapToOrderResponse(SilverPayApiResponse apiResponse) { return new SilverPayOrderResponse { Id = apiResponse.Id, ExternalId = apiResponse.ExternalId, Amount = apiResponse.Amount, Currency = apiResponse.Currency, PaymentAddress = apiResponse.PaymentAddress, Status = apiResponse.Status, CreatedAt = apiResponse.CreatedAt, ExpiresAt = apiResponse.ExpiresAt, PaidAt = apiResponse.PaidAt, PaymentDetails = apiResponse.PaymentDetails, CryptoAmount = apiResponse.CryptoAmount, TransactionHash = apiResponse.TransactionHash, Confirmations = apiResponse.Confirmations }; } // Internal classes for JSON deserialization private class SilverPayApiResponse { [JsonPropertyName("id")] public string Id { get; set; } = string.Empty; [JsonPropertyName("external_id")] public string ExternalId { get; set; } = string.Empty; [JsonPropertyName("amount")] public string AmountString { get; set; } = string.Empty; [JsonIgnore] public decimal Amount => decimal.TryParse(AmountString, out var amount) ? amount : 0; [JsonPropertyName("currency")] public string Currency { get; set; } = string.Empty; [JsonPropertyName("payment_address")] public string PaymentAddress { get; set; } = string.Empty; [JsonPropertyName("status")] public string Status { get; set; } = string.Empty; [JsonPropertyName("created_at")] public DateTime CreatedAt { get; set; } [JsonPropertyName("expires_at")] public DateTime ExpiresAt { get; set; } [JsonPropertyName("paid_at")] public DateTime? PaidAt { get; set; } [JsonPropertyName("payment_details")] public Dictionary? PaymentDetails { get; set; } [JsonPropertyName("crypto_amount")] public string? CryptoAmountString { get; set; } [JsonIgnore] public decimal? CryptoAmount => decimal.TryParse(CryptoAmountString, out var amount) ? amount : null; [JsonPropertyName("tx_hash")] public string? TransactionHash { get; set; } [JsonPropertyName("confirmations")] public int? Confirmations { get; set; } } private class ExchangeRateResponse { [JsonPropertyName("crypto")] public string Crypto { get; set; } = string.Empty; [JsonPropertyName("fiat")] public string Fiat { get; set; } = string.Empty; [JsonPropertyName("rate")] public decimal Rate { get; set; } [JsonPropertyName("timestamp")] public DateTime Timestamp { get; set; } } }