- Fixed invoice creation to use GBP (fiat) instead of cryptocurrency amounts - BTCPay Server now handles automatic crypto conversion - Updated payment flow to use checkout links instead of raw wallet addresses - Added comprehensive logging for debugging payment issues - Created diagnostic endpoints for testing BTCPay connection and payments - Added documentation for deployment and troubleshooting The key issue was that BTCPay v2 manages wallet addresses internally and provides checkout links for customers to complete payments, rather than exposing raw crypto addresses. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
166 lines
6.2 KiB
C#
166 lines
6.2 KiB
C#
using BTCPayServer.Client;
|
|
using BTCPayServer.Client.Models;
|
|
using LittleShop.Enums;
|
|
using Newtonsoft.Json.Linq;
|
|
|
|
namespace LittleShop.Services;
|
|
|
|
public interface IBTCPayServerService
|
|
{
|
|
Task<string> CreateInvoiceAsync(decimal amount, CryptoCurrency currency, string orderId, string? description = null);
|
|
Task<InvoiceData?> GetInvoiceAsync(string invoiceId);
|
|
Task<bool> ValidateWebhookAsync(string payload, string signature);
|
|
}
|
|
|
|
public class BTCPayServerService : IBTCPayServerService
|
|
{
|
|
private readonly BTCPayServerClient _client;
|
|
private readonly IConfiguration _configuration;
|
|
private readonly ILogger<BTCPayServerService> _logger;
|
|
private readonly string _storeId;
|
|
private readonly string _webhookSecret;
|
|
private readonly string _baseUrl;
|
|
|
|
public BTCPayServerService(IConfiguration configuration, ILogger<BTCPayServerService> 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<string> 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<InvoiceData?> GetInvoiceAsync(string invoiceId)
|
|
{
|
|
try
|
|
{
|
|
return await _client.GetInvoice(_storeId, invoiceId);
|
|
}
|
|
catch
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public Task<bool> ValidateWebhookAsync(string payload, string signature)
|
|
{
|
|
try
|
|
{
|
|
// BTCPay Server uses HMAC-SHA256 with format "sha256=<hex>"
|
|
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"
|
|
};
|
|
}
|
|
} |