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 string _storeId; private readonly string _webhookSecret; public BTCPayServerService(IConfiguration configuration) { _configuration = configuration; var 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"] ?? throw new ArgumentException("BTCPayServer:WebhookSecret not configured"); // 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 currencyCode = GetCurrencyCode(currency); var metadata = new JObject { ["orderId"] = orderId, ["currency"] = currencyCode }; if (!string.IsNullOrEmpty(description)) { metadata["itemDesc"] = description; } var request = new CreateInvoiceRequest { Amount = amount, Currency = currencyCode, Metadata = metadata, Checkout = new CreateInvoiceRequest.CheckoutOptions { Expiration = TimeSpan.FromHours(24) } }; try { var invoice = await _client.CreateInvoice(_storeId, request); return invoice.Id; } catch (Exception ex) { // Log the specific error for debugging Console.WriteLine($"BTCPay Server error for {currencyCode}: {ex.Message}"); // Try to continue with real API call for all cryptocurrencies with configured wallets if (currency == CryptoCurrency.BTC || currency == CryptoCurrency.LTC || currency == CryptoCurrency.DASH || currency == CryptoCurrency.XMR) { throw; // Let the calling service handle errors for supported currencies } // For XMR and ETH, we have nodes but BTCPay Server might not be configured yet // Log the error and fall back to placeholder for now Console.WriteLine($"Falling back to placeholder for {currencyCode} - BTCPay Server integration pending"); return $"invoice_{Guid.NewGuid()}"; } } 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" }; } }