180 lines
6.3 KiB
C#
180 lines
6.3 KiB
C#
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<BTCPayWebhookController> _logger;
|
|
|
|
public BTCPayWebhookController(
|
|
ICryptoPaymentService cryptoPaymentService,
|
|
IBTCPayServerService btcPayService,
|
|
IConfiguration configuration,
|
|
ILogger<BTCPayWebhookController> logger)
|
|
{
|
|
_cryptoPaymentService = cryptoPaymentService;
|
|
_btcPayService = btcPayService;
|
|
_configuration = configuration;
|
|
_logger = logger;
|
|
}
|
|
|
|
[HttpPost("webhook")]
|
|
public async Task<IActionResult> 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<BTCPayWebhookDto>(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=<hex>"
|
|
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<bool> 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
|
|
};
|
|
}
|
|
} |