using Microsoft.AspNetCore.Mvc; using System.Security.Cryptography; using System.Text; using System.Text.Json; using LittleShop.DTOs; using LittleShop.Services; using LittleShop.Enums; namespace LittleShop.Controllers; [ApiController] [Route("api/btcpay")] public class BTCPayWebhookController : ControllerBase { private readonly ICryptoPaymentService _cryptoPaymentService; private readonly IBTCPayServerService _btcPayService; private readonly IConfiguration _configuration; private readonly ILogger _logger; public BTCPayWebhookController( ICryptoPaymentService cryptoPaymentService, IBTCPayServerService btcPayService, IConfiguration configuration, ILogger logger) { _cryptoPaymentService = cryptoPaymentService; _btcPayService = btcPayService; _configuration = configuration; _logger = logger; } [HttpPost("webhook")] public async Task ProcessWebhook() { try { // Read the raw request body using var reader = new StreamReader(Request.Body); var requestBody = await reader.ReadToEndAsync(); // Get webhook signature from headers var signature = Request.Headers["BTCPAY-SIG"].FirstOrDefault(); if (string.IsNullOrEmpty(signature)) { _logger.LogWarning("Webhook received without signature"); return BadRequest("Missing webhook signature"); } // Validate webhook signature var webhookSecret = _configuration["BTCPayServer:WebhookSecret"]; if (string.IsNullOrEmpty(webhookSecret)) { _logger.LogError("BTCPay webhook secret not configured"); return StatusCode(500, "Webhook validation not configured"); } if (!ValidateWebhookSignature(requestBody, signature, webhookSecret)) { _logger.LogWarning("Invalid webhook signature"); return BadRequest("Invalid webhook signature"); } // Parse webhook data var webhookData = JsonSerializer.Deserialize(requestBody, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); if (webhookData == null) { _logger.LogWarning("Unable to parse webhook data"); return BadRequest("Invalid webhook data"); } _logger.LogInformation("Processing BTCPay webhook: Type={Type}, InvoiceId={InvoiceId}, StoreId={StoreId}", webhookData.Type, webhookData.InvoiceId, webhookData.StoreId); // Process the webhook based on event type var success = await ProcessWebhookEvent(webhookData); if (!success) { return BadRequest("Failed to process webhook"); } return Ok(); } catch (Exception ex) { _logger.LogError(ex, "Error processing BTCPay webhook"); return StatusCode(500, "Internal server error"); } } private bool ValidateWebhookSignature(string payload, string signature, string secret) { try { // BTCPay Server uses HMAC-SHA256 with format "sha256=" if (!signature.StartsWith("sha256=")) { return false; } var expectedHash = signature.Substring(7); // Remove "sha256=" prefix var secretBytes = Encoding.UTF8.GetBytes(secret); var payloadBytes = Encoding.UTF8.GetBytes(payload); using var hmac = new HMACSHA256(secretBytes); var computedHash = hmac.ComputeHash(payloadBytes); var computedHashHex = Convert.ToHexString(computedHash).ToLowerInvariant(); return expectedHash.Equals(computedHashHex, StringComparison.OrdinalIgnoreCase); } catch (Exception ex) { _logger.LogError(ex, "Error validating webhook signature"); return false; } } private async Task ProcessWebhookEvent(BTCPayWebhookDto webhookData) { try { // Map BTCPay webhook event types to our payment status var paymentStatus = MapWebhookEventToPaymentStatus(webhookData.Type); if (!paymentStatus.HasValue) { _logger.LogInformation("Ignoring webhook event type: {Type}", webhookData.Type); return true; // Not an error, just not a status we care about } // Extract payment details var amount = webhookData.Payment?.PaymentMethodPaid ?? 0; var transactionHash = webhookData.Payment?.TransactionData?.TransactionHash; // Process the payment update var success = await _cryptoPaymentService.ProcessPaymentWebhookAsync( webhookData.InvoiceId, paymentStatus.Value, amount, transactionHash); if (success) { _logger.LogInformation("Successfully processed webhook for invoice {InvoiceId} with status {Status}", webhookData.InvoiceId, paymentStatus.Value); } else { _logger.LogWarning("Failed to process webhook for invoice {InvoiceId}", webhookData.InvoiceId); } return success; } catch (Exception ex) { _logger.LogError(ex, "Error processing webhook event for invoice {InvoiceId}", webhookData.InvoiceId); return false; } } private static PaymentStatus? MapWebhookEventToPaymentStatus(string eventType) { return eventType switch { "InvoiceCreated" => PaymentStatus.Pending, "InvoiceReceivedPayment" => PaymentStatus.Processing, "InvoicePaymentSettled" => PaymentStatus.Completed, "InvoiceProcessing" => PaymentStatus.Processing, "InvoiceExpired" => PaymentStatus.Expired, "InvoiceSettled" => PaymentStatus.Completed, "InvoiceInvalid" => PaymentStatus.Failed, _ => null // Unknown event type }; } }