using BTCPayServer.Client; using BTCPayServer.Client.Models; using LittleShop.Enums; using Newtonsoft.Json.Linq; namespace LittleShop.Services; public interface IBTCPayServerService { Task CreateInvoiceAsync(decimal amount, CryptoCurrency currency, string orderId, string? description = null); Task GetInvoiceAsync(string invoiceId); Task ValidateWebhookAsync(string payload, string signature); } public class BTCPayServerService : IBTCPayServerService { private readonly BTCPayServerClient _client; private readonly IConfiguration _configuration; private readonly ILogger _logger; private readonly string _storeId; private readonly string _webhookSecret; private readonly string _baseUrl; public BTCPayServerService(IConfiguration configuration, ILogger logger) { _configuration = configuration; _logger = logger; _baseUrl = _configuration["BTCPayServer:BaseUrl"] ?? throw new ArgumentException("BTCPayServer:BaseUrl not configured"); var apiKey = _configuration["BTCPayServer:ApiKey"] ?? throw new ArgumentException("BTCPayServer:ApiKey not configured"); _storeId = _configuration["BTCPayServer:StoreId"] ?? throw new ArgumentException("BTCPayServer:StoreId not configured"); _webhookSecret = _configuration["BTCPayServer:WebhookSecret"] ?? ""; _logger.LogInformation("Initializing BTCPay Server connection to {BaseUrl} with Store ID: {StoreId}", _baseUrl, _storeId); // Create HttpClient with certificate bypass for internal networks var httpClient = new HttpClient(new HttpClientHandler() { ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true }); _client = new BTCPayServerClient(new Uri(_baseUrl), apiKey, httpClient); } public async Task CreateInvoiceAsync(decimal amount, CryptoCurrency currency, string orderId, string? description = null) { var paymentMethod = GetPaymentMethod(currency); var metadata = new JObject { ["orderId"] = orderId, ["requestedCurrency"] = currency.ToString(), ["paymentMethod"] = paymentMethod }; if (!string.IsNullOrEmpty(description)) { metadata["itemDesc"] = description; } // Create invoice in GBP (fiat) - BTCPay will handle crypto conversion var request = new CreateInvoiceRequest { Amount = amount, Currency = "GBP", // Always use fiat currency for the amount Metadata = metadata, Checkout = new CreateInvoiceRequest.CheckoutOptions { Expiration = TimeSpan.FromHours(24), PaymentMethods = new[] { paymentMethod }, // Specify which crypto to accept DefaultPaymentMethod = paymentMethod } }; try { _logger.LogDebug("Creating BTCPay invoice - Amount: {Amount} GBP, Payment Method: {PaymentMethod}, Order: {OrderId}", amount, paymentMethod, orderId); var invoice = await _client.CreateInvoice(_storeId, request); _logger.LogInformation("✅ Created BTCPay invoice {InvoiceId} for Order {OrderId} - Amount: {Amount} GBP, Method: {PaymentMethod}, Checkout: {CheckoutLink}", invoice.Id, orderId, amount, paymentMethod, invoice.CheckoutLink); return invoice.Id; } catch (Exception ex) { _logger.LogError(ex, "❌ Failed to create BTCPay invoice - Amount: {Amount} GBP, Method: {PaymentMethod}, Store: {StoreId}, BaseUrl: {BaseUrl}", amount, paymentMethod, _storeId, _baseUrl); // Always throw - never generate fake invoices throw; } } public async Task GetInvoiceAsync(string invoiceId) { try { return await _client.GetInvoice(_storeId, invoiceId); } catch { return null; } } public Task ValidateWebhookAsync(string payload, string signature) { try { // BTCPay Server uses HMAC-SHA256 with format "sha256=" if (!signature.StartsWith("sha256=")) { return Task.FromResult(false); } var expectedHash = signature.Substring(7); // Remove "sha256=" prefix var secretBytes = System.Text.Encoding.UTF8.GetBytes(_webhookSecret); var payloadBytes = System.Text.Encoding.UTF8.GetBytes(payload); using var hmac = new System.Security.Cryptography.HMACSHA256(secretBytes); var computedHash = hmac.ComputeHash(payloadBytes); var computedHashHex = Convert.ToHexString(computedHash).ToLowerInvariant(); return Task.FromResult(expectedHash.Equals(computedHashHex, StringComparison.OrdinalIgnoreCase)); } catch { return Task.FromResult(false); } } private static string GetCurrencyCode(CryptoCurrency currency) { return currency switch { CryptoCurrency.BTC => "BTC", CryptoCurrency.XMR => "XMR", CryptoCurrency.USDT => "USDT", CryptoCurrency.LTC => "LTC", CryptoCurrency.ETH => "ETH", CryptoCurrency.ZEC => "ZEC", CryptoCurrency.DASH => "DASH", CryptoCurrency.DOGE => "DOGE", _ => "BTC" }; } private static string GetPaymentMethod(CryptoCurrency currency) { return currency switch { CryptoCurrency.BTC => "BTC", CryptoCurrency.XMR => "XMR", CryptoCurrency.USDT => "USDT_ETH", // USDT on Ethereum CryptoCurrency.LTC => "LTC", CryptoCurrency.ETH => "ETH", CryptoCurrency.ZEC => "ZEC", CryptoCurrency.DASH => "DASH", CryptoCurrency.DOGE => "DOGE", _ => "BTC" }; } }