Fix BTCPay webhook events and delivery

This commit is contained in:
xmrcheckout
2026-01-08 17:50:09 +00:00
parent 7bde17a3eb
commit af60d1d4c6
3 changed files with 51 additions and 1 deletions
+3
View File
@@ -37,7 +37,9 @@ router = APIRouter()
BTCPAY_PAYMENT_METHOD = "XMR-CHAIN" BTCPAY_PAYMENT_METHOD = "XMR-CHAIN"
BTCPAY_PAYMENT_METHOD_ALIASES = {BTCPAY_PAYMENT_METHOD, "XMR", "XMR_CHAIN"} BTCPAY_PAYMENT_METHOD_ALIASES = {BTCPAY_PAYMENT_METHOD, "XMR", "XMR_CHAIN"}
BTCPAY_WEBHOOK_EVENTS = { BTCPAY_WEBHOOK_EVENTS = {
"InvoiceCreated",
"InvoiceReceivedPayment", "InvoiceReceivedPayment",
"InvoicePaidInFull",
"InvoicePaymentSettled", "InvoicePaymentSettled",
"InvoiceProcessing", "InvoiceProcessing",
"InvoiceExpired", "InvoiceExpired",
@@ -323,6 +325,7 @@ def create_invoice(
except HTTPException: except HTTPException:
pass pass
dispatch_webhooks(db, str(user.id), "invoice.created", invoice) dispatch_webhooks(db, str(user.id), "invoice.created", invoice)
dispatch_btcpay_webhooks(db, str(user.id), "InvoiceCreated", invoice)
status_name, additional_status = _btcpay_status(invoice) status_name, additional_status = _btcpay_status(invoice)
amount, currency, _ = _btcpay_amount_currency(invoice) amount, currency, _ = _btcpay_amount_currency(invoice)
expiration_time = _epoch_seconds(invoice.expires_at) expiration_time = _epoch_seconds(invoice.expires_at)
+47 -1
View File
@@ -6,6 +6,7 @@ import json
import logging import logging
import time import time
from datetime import datetime, timezone from datetime import datetime, timezone
from urllib.parse import urljoin
import requests import requests
from requests import RequestException from requests import RequestException
@@ -16,6 +17,8 @@ from .security import decrypt_secret
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_REDIRECT_STATUSES = {301, 302, 303, 307, 308}
def dispatch_btcpay_webhooks( def dispatch_btcpay_webhooks(
db: Session, db: Session,
@@ -51,8 +54,25 @@ def dispatch_btcpay_webhooks(
headers = { headers = {
"BTCPay-Sig": f"sha256={signature}", "BTCPay-Sig": f"sha256={signature}",
"Content-Type": "application/json", "Content-Type": "application/json",
"User-Agent": "xmrcheckout-btcpay-webhook/1.0",
} }
requests.post(hook.url, data=body, headers=headers, timeout=5) response = _post_with_redirects(
hook.url,
data=body,
headers=headers,
timeout=5,
)
if response is None:
continue
if response.status_code >= 400:
logger.warning(
"BTCPay webhook delivered non-success status",
extra={
"webhook_id": str(hook.id),
"event": event_type,
"http_status": response.status_code,
},
)
except RequestException as exc: except RequestException as exc:
logger.warning( logger.warning(
"BTCPay webhook delivery failed", "BTCPay webhook delivery failed",
@@ -109,3 +129,29 @@ def _after_expiration(invoice: Invoice) -> bool:
def _sign_payload(body: bytes, secret: str) -> str: def _sign_payload(body: bytes, secret: str) -> str:
return hmac.new(secret.encode("utf-8"), body, hashlib.sha256).hexdigest() return hmac.new(secret.encode("utf-8"), body, hashlib.sha256).hexdigest()
def _post_with_redirects(
url: str,
*,
data: bytes,
headers: dict[str, str],
timeout: int,
max_redirects: int = 3,
) -> requests.Response | None:
current_url = url
for _ in range(max_redirects + 1):
response = requests.post(
current_url,
data=data,
headers=headers,
timeout=timeout,
allow_redirects=False,
)
if response.status_code not in _REDIRECT_STATUSES:
return response
location = response.headers.get("Location")
if not location:
return response
current_url = urljoin(current_url, location)
return None
+1
View File
@@ -178,6 +178,7 @@ def _reconcile_invoices(service: MoneroWalletService) -> None:
dispatch_btcpay_webhooks( dispatch_btcpay_webhooks(
db, str(user.id), "InvoiceReceivedPayment", invoice db, str(user.id), "InvoiceReceivedPayment", invoice
) )
dispatch_btcpay_webhooks(db, str(user.id), "InvoicePaidInFull", invoice)
dispatch_btcpay_webhooks( dispatch_btcpay_webhooks(
db, str(user.id), "InvoiceProcessing", invoice db, str(user.id), "InvoiceProcessing", invoice
) )