littleshop/LittleShop/Services/SilverPayService.cs
SysAdmin caff08cb6f Deploy LittleShop to Hostinger with Docker and BunkerWeb
- Updated Docker configuration for production deployment
- Added SilverPay integration settings
- Configured for admin.thebankofdebbie.giize.com deployment
- Includes all recent security fixes and improvements

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-24 13:00:17 +01:00

403 lines
15 KiB
C#

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<SilverPayService> _logger;
private readonly IServiceProvider _serviceProvider;
public SilverPayService(
HttpClient httpClient,
IConfiguration configuration,
IServiceProvider serviceProvider,
ILogger<SilverPayService> 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<ISystemSettingsService>();
// 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<SilverPayOrderResponse> 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<ISystemSettingsService>();
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<SilverPayApiResponse>(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<SilverPayOrderResponse?> 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<SilverPayApiResponse>(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<bool> 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<decimal?> 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<ExchangeRateResponse>(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<List<string>> 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<string> { "BTC", "ETH", "USDT", "LTC" };
}
var json = await response.Content.ReadAsStringAsync();
var currencies = JsonSerializer.Deserialize<List<string>>(json, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
return currencies ?? new List<string> { "BTC" };
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting supported currencies from SilverPAY");
// Return a safe default
return new List<string> { "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<string, object>? 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; }
}
}