chore: cleanup unused code, update docs for payment integration
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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`
|
||||||
|
|||||||
@@ -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 |
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const CACHE_NAME = 'dgray-v42';
|
const CACHE_NAME = 'dgray-v43';
|
||||||
const STATIC_ASSETS = [
|
const STATIC_ASSETS = [
|
||||||
'/',
|
'/',
|
||||||
'/index.html',
|
'/index.html',
|
||||||
|
|||||||
Reference in New Issue
Block a user