From 3c7d475d3661167ef30572428457266ccffe1ac3 Mon Sep 17 00:00:00 2001 From: Alexander Schmidt Date: Fri, 6 Feb 2026 16:23:23 +0100 Subject: [PATCH] chore: cleanup unused code, update docs for payment integration --- AGENTS.md | 2 +- README.md | 9 +++- docs/MONETIZATION.md | 29 ++++++++--- docs/pow-server/README.md | 8 +++ docs/pow-server/btcpay-webhook.php | 76 +++++++++++++--------------- js/components/pages/page-create.js | 79 +++++++++--------------------- service-worker.js | 2 +- 7 files changed, 97 insertions(+), 108 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 87e838e..1b1300b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -62,7 +62,7 @@ js/ │ ├── crypto.js # NaCl Encryption │ ├── currency.js # XMR/Fiat Umrechnung │ ├── pow-captcha.js # Proof-of-Work Captcha (Challenge/Verify) -│ └── btcpay.js # BTCPay Server Integration (Invoice, Checkout, Polling) +│ └── btcpay.js # BTCPay Server Integration (Invoice, Checkout, Webhook) └── components/ ├── app-shell.js # Layout, registriert Routes ├── app-header.js # Header (Theme-Toggle, Lang-Dropdown, Profil-Dropdown) diff --git a/README.md b/README.md index 2045081..00e942c 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,8 @@ dgray.io ermöglicht es Nutzern, Kleinanzeigen zu schalten und Waren/Dienstleist ### Services - **Directus** Backend: `api.dgray.io` (Docker) -- **PoW Captcha**: `pow.dgray.io` (PHP, HMAC-signierte Challenges) +- **PoW Captcha + Payment Proxy**: `pow.dgray.io` (PHP, HMAC-signierte Challenges, BTCPay Proxy + Webhook) +- **BTCPay Server**: `pay.xmr.rocks` (Monero-Zahlungen, Trocador-Plugin) - **TweetNaCl**: Self-hosted in `js/vendor/` (E2E-Verschlüsselung) ### Infrastruktur (geplant) @@ -152,7 +153,8 @@ dgray/ │ │ ├── conversations.js# Zero-Knowledge Chat │ │ ├── crypto.js # NaCl Encryption (box.before + secretbox) │ │ ├── currency.js # XMR/Fiat Umrechnung -│ │ └── pow-captcha.js # PoW Captcha (Server-first, lokaler Fallback) +│ │ ├── pow-captcha.js # PoW Captcha (Server-first, lokaler Fallback) +│ │ └── btcpay.js # BTCPay Server Integration (Invoice, Checkout) │ ├── vendor/ │ │ ├── nacl-fast.min.js # TweetNaCl (self-hosted) │ │ ├── nacl-util.min.js # TweetNaCl Utils @@ -218,6 +220,9 @@ dgray/ ### Phase 4: Payments - [x] XMR-Kursabfrage API (CoinGecko) - [x] Fiat ↔ XMR Umrechnung (Dual-Preis-Anzeige) +- [x] BTCPay Server Integration (`pay.xmr.rocks`, Proxy auf `pow.dgray.io`) +- [x] Listing-Gebühr: 1 EUR/USD/CHF/GBP (200 JPY) via Monero +- [x] Webhook für Auto-Publish nach Blockchain-Confirmation - [ ] Wallet-Anbindung (monero-wallet-rpc) - [ ] MultiSig Escrow diff --git a/docs/MONETIZATION.md b/docs/MONETIZATION.md index 7408e2d..b038a40 100644 --- a/docs/MONETIZATION.md +++ b/docs/MONETIZATION.md @@ -50,15 +50,30 @@ - **Provider**: BTCpay Server (self-hosted) - **URL**: https://pay.xmr.rocks/ +- **Proxy**: `pow.dgray.io` — alle API-Aufrufe laufen über den PHP-Proxy (BTCPay API-Key bleibt serverseitig) - **Primär**: Monero (XMR) - **Alternativ**: Andere Kryptos via Trocador-Plugin (automatischer Swap zu XMR) - **Preisumrechnung**: Live XMR-Kurs via Kraken API - - EUR: `https://api.kraken.com/0/public/Ticker?pair=XMREUR` - - USD: `https://api.kraken.com/0/public/Ticker?pair=XMRUSD` - - CHF: `https://api.kraken.com/0/public/Ticker?pair=XMRCHF` - - GBP: `https://api.kraken.com/0/public/Ticker?pair=XMRGBP` - - JPY: `https://api.kraken.com/0/public/Ticker?pair=XMRJPY` -- **Bestätigung**: Nach 1-2 Blockchain-Confirmations + - EUR, USD, CHF, GBP, JPY +- **Bestätigung**: Nach 1 Blockchain-Confirmation (via Webhook) + +### Flow: Draft → Processing → Published + +1. User erstellt Listing → wird als `draft` mit `payment_status: unpaid` gespeichert +2. BTCPay Invoice wird über `pow.dgray.io/btcpay/invoice` erstellt +3. BTCPay Checkout-Modal öffnet sich im Frontend (`js/services/btcpay.js`) +4. Nach Zahlung: + - **Frontend**: Prüft Status via `pow.dgray.io/btcpay/status` nach Modal-Close + - **Webhook**: `pow.dgray.io/btcpay/webhook` empfängt BTCPay Events, setzt `status: published` + `payment_status: paid` nach 1 Confirmation +5. Listing wird veröffentlicht (30 Tage Laufzeit, `expires_at` wird gesetzt) + +### Endpunkte (pow.dgray.io) + +| Endpoint | Methode | Beschreibung | +|----------|---------|-------------| +| `/btcpay/invoice` | POST | Invoice erstellen (listingId, currency) | +| `/btcpay/status?id={id}` | GET | Invoice-Status abfragen | +| `/btcpay/webhook` | POST | BTCPay Webhook (auto-publish nach Confirmation) | ## Offene Fragen @@ -66,3 +81,5 @@ - [x] ~~XMR-Kurs API für Umrechnung~~ → Kraken API - [x] ~~Anzahl Deals für Power-User Status~~ → 5/15/50 Stufen - [x] ~~Captcha-Lösung~~ → Eigenes PoW-Captcha (keine Lizenzkosten) +- [x] ~~Payment-Proxy~~ → `pow.dgray.io` (PHP, API-Key serverseitig) +- [x] ~~Webhook für Auto-Publish~~ → `btcpay-webhook.php` auf `pow.dgray.io` diff --git a/docs/pow-server/README.md b/docs/pow-server/README.md index 4e481e6..51e6fb6 100644 --- a/docs/pow-server/README.md +++ b/docs/pow-server/README.md @@ -73,6 +73,14 @@ Response: } ``` +### POST /btcpay/webhook +Empfängt BTCPay Server Webhook-Events. Wird in BTCPay unter Store → Settings → Webhooks konfiguriert. + +- **URL**: `https://pow.dgray.io/btcpay/webhook` +- **Event**: `InvoiceSettled` (nach 1 Blockchain-Confirmation) +- **Aktion**: Setzt das zugehörige Listing in Directus auf `status: published`, `payment_status: paid`, setzt `paid_at` und `expires_at` (30 Tage) +- **Sicherheit**: Webhook-Secret wird serverseitig geprüft + ## Gebühren | Währung | Betrag | diff --git a/docs/pow-server/btcpay-webhook.php b/docs/pow-server/btcpay-webhook.php index f6a431a..339d019 100644 --- a/docs/pow-server/btcpay-webhook.php +++ b/docs/pow-server/btcpay-webhook.php @@ -19,7 +19,7 @@ $payload = json_decode($rawBody, true); if ($payload === null) { http_response_code(400); - echo json_encode(['error' => 'Invalid JSON', 'raw' => substr($rawBody, 0, 500)]); + echo json_encode(['error' => 'Invalid JSON']); exit; } @@ -36,11 +36,10 @@ if (BTCPAY_WEBHOOK_SECRET) { } $type = $payload['type'] ?? null; -$invoiceId = $payload['invoiceId'] ?? null; if (!$type) { http_response_code(400); - echo json_encode(['error' => 'Missing type', 'keys' => array_keys($payload ?: [])]); + echo json_encode(['error' => 'Missing type']); exit; } @@ -50,35 +49,11 @@ if ($type !== 'InvoiceSettled' && $type !== 'InvoicePaymentSettled') { exit; } -// For settled events, invoiceId is required -if (!$invoiceId) { - echo json_encode(['ok' => true, 'action' => 'test_acknowledged', 'type' => $type]); - exit; -} - -// Fetch invoice from BTCPay to get listing ID from metadata -$btcpayUrl = BTCPAY_BASE_URL . '/api/v1/stores/' . BTCPAY_STORE_ID . '/invoices/' . urlencode($invoiceId); -$btcpayContext = stream_context_create([ - 'http' => [ - 'method' => 'GET', - 'header' => "Authorization: token " . BTCPAY_API_KEY . "\r\n", - 'ignore_errors' => true, - 'timeout' => 10, - ], -]); - -$btcpayResponse = file_get_contents($btcpayUrl, false, $btcpayContext); -if ($btcpayResponse === false) { - http_response_code(502); - echo json_encode(['error' => 'Failed to fetch invoice from BTCPay']); - exit; -} - -$invoice = json_decode($btcpayResponse, true); -$listingId = $invoice['metadata']['listingId'] ?? null; +// Read listingId directly from webhook payload metadata +$listingId = $payload['metadata']['listingId'] ?? null; if (!$listingId) { - echo json_encode(['ok' => true, 'action' => 'skipped', 'reason' => 'No listingId in invoice metadata']); + echo json_encode(['ok' => true, 'action' => 'skipped', 'reason' => 'No listingId in metadata']); exit; } @@ -94,26 +69,43 @@ $directusPayload = json_encode([ ]); $directusUrl = DIRECTUS_URL . '/items/listings/' . urlencode($listingId); -$directusContext = stream_context_create([ - 'http' => [ - 'method' => 'PATCH', - 'header' => "Content-Type: application/json\r\nAuthorization: Bearer " . DIRECTUS_TOKEN . "\r\n", - 'content' => $directusPayload, - 'ignore_errors' => true, - 'timeout' => 10, + +$ch = curl_init($directusUrl); +curl_setopt_array($ch, [ + CURLOPT_CUSTOMREQUEST => 'PATCH', + CURLOPT_POSTFIELDS => $directusPayload, + CURLOPT_HTTPHEADER => [ + 'Content-Type: application/json', + 'Authorization: Bearer ' . DIRECTUS_TOKEN, ], + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 10, + CURLOPT_CONNECTTIMEOUT => 5, ]); -$directusResponse = file_get_contents($directusUrl, false, $directusContext); +$directusResponse = curl_exec($ch); +$directusStatus = curl_getinfo($ch, CURLINFO_HTTP_CODE); +$curlError = curl_error($ch); +curl_close($ch); -$directusStatus = 500; -if (isset($http_response_header[0]) && preg_match('/\d{3}/', $http_response_header[0], $matches)) { - $directusStatus = (int)$matches[0]; +if ($directusResponse === false || $curlError) { + http_response_code(502); + echo json_encode([ + 'error' => 'Connection to Directus failed', + 'listingId' => $listingId, + 'curl_error' => $curlError, + ]); + exit; } if ($directusStatus >= 400) { http_response_code(502); - echo json_encode(['error' => 'Failed to update listing in Directus', 'status' => $directusStatus]); + echo json_encode([ + 'error' => 'Directus returned error', + 'status' => $directusStatus, + 'listingId' => $listingId, + 'response' => substr($directusResponse, 0, 500), + ]); exit; } diff --git a/js/components/pages/page-create.js b/js/components/pages/page-create.js index b285a9a..454fe0b 100644 --- a/js/components/pages/page-create.js +++ b/js/components/pages/page-create.js @@ -4,7 +4,7 @@ import { auth } from '../../services/auth.js' import { directus } from '../../services/directus.js' import { categoriesService } from '../../services/categories.js' import { SUPPORTED_CURRENCIES, getDisplayCurrency } from '../../services/currency.js' -import { createInvoice, openCheckout, pollUntilDone, getPendingInvoice, savePendingInvoice, clearPendingInvoice, getInvoiceStatus } from '../../services/btcpay.js' +import { createInvoice, openCheckout, getPendingInvoice, savePendingInvoice, clearPendingInvoice, getInvoiceStatus } from '../../services/btcpay.js' import { escapeHTML } from '../../utils/helpers.js' import '../location-picker.js' import '../pow-captcha.js' @@ -723,46 +723,36 @@ class PageCreate extends HTMLElement { } async onPaymentSuccess(listingId) { - const days = 30 - const expiresAt = new Date() - expiresAt.setDate(expiresAt.getDate() + days) + try { + const days = 30 + const expiresAt = new Date() + expiresAt.setDate(expiresAt.getDate() + days) - await directus.updateListing(listingId, { - status: 'published', - payment_status: 'paid', - paid_at: new Date().toISOString(), - expires_at: expiresAt.toISOString() - }) + await directus.updateListing(listingId, { + status: 'published', + payment_status: 'paid', + paid_at: new Date().toISOString(), + expires_at: expiresAt.toISOString() + }) + } catch (e) { + console.warn('Could not update listing status, webhook will handle it:', e) + } clearPendingInvoice(listingId) - router.navigate(`/listing/${listingId}`) + router.navigate('/my-listings') } async onPaymentReceived(listingId) { - await directus.updateListing(listingId, { - payment_status: 'processing' - }) + try { + await directus.updateListing(listingId, { + payment_status: 'processing' + }) + } catch (e) { + console.warn('Could not update listing status, webhook will handle it:', e) + } clearPendingInvoice(listingId) - this.showPaymentSuccess() - } - - showPaymentSuccess() { - this.innerHTML = /* html */` -
-
-
- - - - -
-

${t('payment.processing')}

-

${t('payment.awaitingHint')}

- ${t('profile.myListings')} -
-
- ` + router.navigate('/my-listings') } showError(message) { @@ -1011,28 +1001,5 @@ style.textContent = /* css */` page-create .btn-link:hover { color: var(--color-primary-hover); } - - page-create .payment-success { - text-align: center; - padding: var(--space-3xl) 0; - } - - page-create .success-icon { - color: var(--color-warning, #e6a700); - margin-bottom: var(--space-lg); - } - - page-create .payment-success h2 { - margin-bottom: var(--space-md); - } - - page-create .payment-success p { - color: var(--color-text-secondary); - margin-bottom: var(--space-xl); - max-width: 400px; - margin-left: auto; - margin-right: auto; - line-height: 1.5; - } ` document.head.appendChild(style) diff --git a/service-worker.js b/service-worker.js index c8e2c1b..819f565 100644 --- a/service-worker.js +++ b/service-worker.js @@ -1,4 +1,4 @@ -const CACHE_NAME = 'dgray-v42'; +const CACHE_NAME = 'dgray-v43'; const STATIC_ASSETS = [ '/', '/index.html',