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 string _baseUrl; private readonly string _apiKey; private readonly string _webhookSecret; public SilverPayService( HttpClient httpClient, IConfiguration configuration, ILogger logger) { _httpClient = httpClient; _configuration = configuration; _logger = logger; _baseUrl = _configuration["SilverPay:BaseUrl"] ?? throw new ArgumentException("SilverPay:BaseUrl not configured"); _apiKey = _configuration["SilverPay:ApiKey"] ?? ""; _webhookSecret = _configuration["SilverPay:WebhookSecret"] ?? ""; // Configure HTTP client _httpClient.BaseAddress = new Uri(_baseUrl); _httpClient.DefaultRequestHeaders.Add("Accept", "application/json"); if (!string.IsNullOrEmpty(_apiKey)) { _httpClient.DefaultRequestHeaders.Add("X-API-Key", _apiKey); } _logger.LogInformation("Initialized SilverPAY connection to {BaseUrl}", _baseUrl); } public async Task CreateOrderAsync( string externalId, decimal amount, CryptoCurrency currency, string? description = null, string? webhookUrl = null) { try { var currencyCode = GetSilverPayCurrency(currency); // Prepare request body for SilverPAY var request = new { external_id = externalId, fiat_amount = amount, // Amount in GBP fiat_currency = "GBP", currency = currencyCode, webhook_url = webhookUrl ?? _configuration["SilverPay:DefaultWebhookUrl"], expires_in_hours = 24 }; var json = JsonSerializer.Serialize(request, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }); var content = new StringContent(json, Encoding.UTF8, "application/json"); _logger.LogDebug("Creating SilverPAY order - External ID: {ExternalId}, Amount: {Amount} GBP, Currency: {Currency}", externalId, amount, currencyCode); // 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 { 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 Task ValidateWebhookAsync(string payload, string signature) { try { // 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 Task.FromResult(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 Task.FromResult(computedHashHex.Equals(expectedHash, StringComparison.OrdinalIgnoreCase)); } catch (Exception ex) { _logger.LogError(ex, "Error validating webhook signature"); return Task.FromResult(false); } } public async Task GetExchangeRateAsync(string cryptoCurrency, string fiatCurrency = "GBP") { try { 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; } } 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 decimal Amount { get; set; } [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 decimal? CryptoAmount { get; set; } [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; } } }