chore: cleanup unused code, update docs for payment integration
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,6 +723,7 @@ class PageCreate extends HTMLElement {
|
||||
}
|
||||
|
||||
async onPaymentSuccess(listingId) {
|
||||
try {
|
||||
const days = 30
|
||||
const expiresAt = new Date()
|
||||
expiresAt.setDate(expiresAt.getDate() + days)
|
||||
@@ -733,36 +734,25 @@ class PageCreate extends HTMLElement {
|
||||
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) {
|
||||
try {
|
||||
await directus.updateListing(listingId, {
|
||||
payment_status: 'processing'
|
||||
})
|
||||
|
||||
clearPendingInvoice(listingId)
|
||||
this.showPaymentSuccess()
|
||||
} catch (e) {
|
||||
console.warn('Could not update listing status, webhook will handle it:', e)
|
||||
}
|
||||
|
||||
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>
|
||||
`
|
||||
clearPendingInvoice(listingId)
|
||||
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)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const CACHE_NAME = 'dgray-v42';
|
||||
const CACHE_NAME = 'dgray-v43';
|
||||
const STATIC_ASSETS = [
|
||||
'/',
|
||||
'/index.html',
|
||||
|
||||
Reference in New Issue
Block a user