chore: cleanup unused code, update docs for payment integration

This commit is contained in:
2026-02-06 16:23:23 +01:00
parent 52634f84bf
commit 3c7d475d36
7 changed files with 97 additions and 108 deletions

View File

@@ -62,7 +62,7 @@ js/
│ ├── crypto.js # NaCl Encryption │ ├── crypto.js # NaCl Encryption
│ ├── currency.js # XMR/Fiat Umrechnung │ ├── currency.js # XMR/Fiat Umrechnung
│ ├── pow-captcha.js # Proof-of-Work Captcha (Challenge/Verify) │ ├── 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/ └── components/
├── app-shell.js # Layout, registriert Routes ├── app-shell.js # Layout, registriert Routes
├── app-header.js # Header (Theme-Toggle, Lang-Dropdown, Profil-Dropdown) ├── app-header.js # Header (Theme-Toggle, Lang-Dropdown, Profil-Dropdown)

View File

@@ -67,7 +67,8 @@ dgray.io ermöglicht es Nutzern, Kleinanzeigen zu schalten und Waren/Dienstleist
### Services ### Services
- **Directus** Backend: `api.dgray.io` (Docker) - **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) - **TweetNaCl**: Self-hosted in `js/vendor/` (E2E-Verschlüsselung)
### Infrastruktur (geplant) ### Infrastruktur (geplant)
@@ -152,7 +153,8 @@ dgray/
│ │ ├── conversations.js# Zero-Knowledge Chat │ │ ├── conversations.js# Zero-Knowledge Chat
│ │ ├── crypto.js # NaCl Encryption (box.before + secretbox) │ │ ├── crypto.js # NaCl Encryption (box.before + secretbox)
│ │ ├── currency.js # XMR/Fiat Umrechnung │ │ ├── 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/ │ ├── vendor/
│ │ ├── nacl-fast.min.js # TweetNaCl (self-hosted) │ │ ├── nacl-fast.min.js # TweetNaCl (self-hosted)
│ │ ├── nacl-util.min.js # TweetNaCl Utils │ │ ├── nacl-util.min.js # TweetNaCl Utils
@@ -218,6 +220,9 @@ dgray/
### Phase 4: Payments ### Phase 4: Payments
- [x] XMR-Kursabfrage API (CoinGecko) - [x] XMR-Kursabfrage API (CoinGecko)
- [x] Fiat ↔ XMR Umrechnung (Dual-Preis-Anzeige) - [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) - [ ] Wallet-Anbindung (monero-wallet-rpc)
- [ ] MultiSig Escrow - [ ] MultiSig Escrow

View File

@@ -50,15 +50,30 @@
- **Provider**: BTCpay Server (self-hosted) - **Provider**: BTCpay Server (self-hosted)
- **URL**: https://pay.xmr.rocks/ - **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) - **Primär**: Monero (XMR)
- **Alternativ**: Andere Kryptos via Trocador-Plugin (automatischer Swap zu XMR) - **Alternativ**: Andere Kryptos via Trocador-Plugin (automatischer Swap zu XMR)
- **Preisumrechnung**: Live XMR-Kurs via Kraken API - **Preisumrechnung**: Live XMR-Kurs via Kraken API
- EUR: `https://api.kraken.com/0/public/Ticker?pair=XMREUR` - EUR, USD, CHF, GBP, JPY
- USD: `https://api.kraken.com/0/public/Ticker?pair=XMRUSD` - **Bestätigung**: Nach 1 Blockchain-Confirmation (via Webhook)
- CHF: `https://api.kraken.com/0/public/Ticker?pair=XMRCHF`
- GBP: `https://api.kraken.com/0/public/Ticker?pair=XMRGBP` ### Flow: Draft → Processing → Published
- JPY: `https://api.kraken.com/0/public/Ticker?pair=XMRJPY`
- **Bestätigung**: Nach 1-2 Blockchain-Confirmations 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 ## Offene Fragen
@@ -66,3 +81,5 @@
- [x] ~~XMR-Kurs API für Umrechnung~~ → Kraken API - [x] ~~XMR-Kurs API für Umrechnung~~ → Kraken API
- [x] ~~Anzahl Deals für Power-User Status~~ → 5/15/50 Stufen - [x] ~~Anzahl Deals für Power-User Status~~ → 5/15/50 Stufen
- [x] ~~Captcha-Lösung~~ → Eigenes PoW-Captcha (keine Lizenzkosten) - [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`

View File

@@ -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 ## Gebühren
| Währung | Betrag | | Währung | Betrag |

View File

@@ -19,7 +19,7 @@ $payload = json_decode($rawBody, true);
if ($payload === null) { if ($payload === null) {
http_response_code(400); http_response_code(400);
echo json_encode(['error' => 'Invalid JSON', 'raw' => substr($rawBody, 0, 500)]); echo json_encode(['error' => 'Invalid JSON']);
exit; exit;
} }
@@ -36,11 +36,10 @@ if (BTCPAY_WEBHOOK_SECRET) {
} }
$type = $payload['type'] ?? null; $type = $payload['type'] ?? null;
$invoiceId = $payload['invoiceId'] ?? null;
if (!$type) { if (!$type) {
http_response_code(400); http_response_code(400);
echo json_encode(['error' => 'Missing type', 'keys' => array_keys($payload ?: [])]); echo json_encode(['error' => 'Missing type']);
exit; exit;
} }
@@ -50,35 +49,11 @@ if ($type !== 'InvoiceSettled' && $type !== 'InvoicePaymentSettled') {
exit; exit;
} }
// For settled events, invoiceId is required // Read listingId directly from webhook payload metadata
if (!$invoiceId) { $listingId = $payload['metadata']['listingId'] ?? null;
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;
if (!$listingId) { 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; exit;
} }
@@ -94,26 +69,43 @@ $directusPayload = json_encode([
]); ]);
$directusUrl = DIRECTUS_URL . '/items/listings/' . urlencode($listingId); $directusUrl = DIRECTUS_URL . '/items/listings/' . urlencode($listingId);
$directusContext = stream_context_create([
'http' => [ $ch = curl_init($directusUrl);
'method' => 'PATCH', curl_setopt_array($ch, [
'header' => "Content-Type: application/json\r\nAuthorization: Bearer " . DIRECTUS_TOKEN . "\r\n", CURLOPT_CUSTOMREQUEST => 'PATCH',
'content' => $directusPayload, CURLOPT_POSTFIELDS => $directusPayload,
'ignore_errors' => true, CURLOPT_HTTPHEADER => [
'timeout' => 10, '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 ($directusResponse === false || $curlError) {
if (isset($http_response_header[0]) && preg_match('/\d{3}/', $http_response_header[0], $matches)) { http_response_code(502);
$directusStatus = (int)$matches[0]; echo json_encode([
'error' => 'Connection to Directus failed',
'listingId' => $listingId,
'curl_error' => $curlError,
]);
exit;
} }
if ($directusStatus >= 400) { if ($directusStatus >= 400) {
http_response_code(502); 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; exit;
} }

View File

@@ -4,7 +4,7 @@ import { auth } from '../../services/auth.js'
import { directus } from '../../services/directus.js' import { directus } from '../../services/directus.js'
import { categoriesService } from '../../services/categories.js' import { categoriesService } from '../../services/categories.js'
import { SUPPORTED_CURRENCIES, getDisplayCurrency } from '../../services/currency.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 { escapeHTML } from '../../utils/helpers.js'
import '../location-picker.js' import '../location-picker.js'
import '../pow-captcha.js' import '../pow-captcha.js'
@@ -723,46 +723,36 @@ class PageCreate extends HTMLElement {
} }
async onPaymentSuccess(listingId) { async onPaymentSuccess(listingId) {
const days = 30 try {
const expiresAt = new Date() const days = 30
expiresAt.setDate(expiresAt.getDate() + days) const expiresAt = new Date()
expiresAt.setDate(expiresAt.getDate() + days)
await directus.updateListing(listingId, { await directus.updateListing(listingId, {
status: 'published', status: 'published',
payment_status: 'paid', payment_status: 'paid',
paid_at: new Date().toISOString(), paid_at: new Date().toISOString(),
expires_at: expiresAt.toISOString() expires_at: expiresAt.toISOString()
}) })
} catch (e) {
console.warn('Could not update listing status, webhook will handle it:', e)
}
clearPendingInvoice(listingId) clearPendingInvoice(listingId)
router.navigate(`/listing/${listingId}`) router.navigate('/my-listings')
} }
async onPaymentReceived(listingId) { async onPaymentReceived(listingId) {
await directus.updateListing(listingId, { try {
payment_status: 'processing' await directus.updateListing(listingId, {
}) payment_status: 'processing'
})
} catch (e) {
console.warn('Could not update listing status, webhook will handle it:', e)
}
clearPendingInvoice(listingId) clearPendingInvoice(listingId)
this.showPaymentSuccess() router.navigate('/my-listings')
}
showPaymentSuccess() {
this.innerHTML = /* html */`
<div class="create-page">
<div class="payment-success">
<div class="success-icon">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<polyline points="12 6 12 12 16 14"></polyline>
</svg>
</div>
<h2>${t('payment.processing')}</h2>
<p>${t('payment.awaitingHint')}</p>
<a href="#/my-listings" class="btn btn-primary">${t('profile.myListings')}</a>
</div>
</div>
`
} }
showError(message) { showError(message) {
@@ -1011,28 +1001,5 @@ style.textContent = /* css */`
page-create .btn-link:hover { page-create .btn-link:hover {
color: var(--color-primary-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) document.head.appendChild(style)

View File

@@ -1,4 +1,4 @@
const CACHE_NAME = 'dgray-v42'; const CACHE_NAME = 'dgray-v43';
const STATIC_ASSETS = [ const STATIC_ASSETS = [
'/', '/',
'/index.html', '/index.html',