Skip to content

Commit 7c094e9

Browse files
committed
feat: donation mode — accept shielded Zcash donations
Backend: - Extend payment_links with mode, donation_config, total_raised columns - Extend invoices with payment_link_id, is_donation, campaign_counted - Make price_id nullable for donation links (no product required) - Add POST /donation-links, GET /payment-links/{slug}/info endpoints - Fork resolve endpoint for donation vs payment flow - Increment total_raised on confirmed donation (idempotent via campaign_counted) - Support editing donation_config via PATCH /payment-links/{id} - Add cover_image_position field for image focus control - Add validation for all donation config fields Frontend roadmap update with Phase 3.5 / 3.6 donation infrastructure.
1 parent ad6dc40 commit 7c094e9

8 files changed

Lines changed: 679 additions & 42 deletions

File tree

ROADMAP.md

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,19 @@ Privacy-preserving Zcash payment gateway. Non-custodial, shielded-only.
4141

4242
## Phase 2 -- Performance & Real-Time
4343

44-
- [ ] **Parallel trial decryption** with `rayon` (.par_iter() over merchants x actions)
45-
- [ ] **CipherScan WebSocket stream** (`ws://api.cipherscan.app/mempool/stream`)
46-
- Push raw tx hex as Zebra sees new txids
47-
- Eliminates polling latency entirely
48-
- Single persistent connection per CipherPay instance
44+
- [x] **CipherScan WebSocket stream** (real-time mempool push via service key)
45+
- CipherScan receives mempool events from Zebra gRPC indexer
46+
- Service clients subscribe to `raw_mempool` channel via `X-Service-Key` header
47+
- Raw tx hex pushed to CipherPay on every mempool event — zero HTTP overhead
48+
- Sub-second payment detection (was 5s polling)
49+
- 30s polling retained as resilience fallback
50+
- Auto-reconnect with exponential backoff (3s → 30s cap)
51+
- [ ] **hasOrchard early filter** — skip non-Orchard txs before trial decryption (CipherPay side). Quick win at scale.
52+
- [ ] **Cached pending invoices + merchant keys** — refresh every 2–5s instead of per-WS-push DB query. Removes SQLite bottleneck under high mempool throughput.
53+
- [ ] **Parallel trial decryption** with `rayon` (.par_iter() over merchants × actions). Near-linear speedup across CPU cores.
4954
- [ ] **CipherScan batch raw tx endpoint** (`POST /api/tx/raw/batch`)
5055
- Accept array of txids, return array of hex
51-
- Single HTTP round-trip instead of N calls
56+
- Single HTTP round-trip instead of N calls (for polling fallback path)
5257
- [ ] Mempool deduplication improvements (bloom filter for seen txids)
5358
- [ ] Sapling trial decryption support (currently Orchard-only)
5459
- [ ] Scanner metrics (Prometheus endpoint: decryption rate, latency, match rate)
@@ -74,6 +79,50 @@ Privacy-preserving Zcash payment gateway. Non-custodial, shielded-only.
7479
- [ ] Multi-currency display (EUR, USD, GBP with locked ZEC rate)
7580
- [ ] Email notifications (optional, privacy-conscious: no PII in email body)
7681

82+
## Phase 3.5 -- Donation Infrastructure
83+
84+
- [ ] **Donation mode for payment links**`mode` column on `payment_links` ('payment' | 'donation')
85+
- Donation links have no `price_id` — amount chosen by donor at checkout
86+
- `donation_config` JSON: mission statement, suggested amounts, thank-you message, campaign name/goal
87+
- `total_raised` counter incremented on confirmation
88+
- `payment_link_id` FK on invoices for campaign progress tracking
89+
- `is_donation` flag on invoices for checkout UI adaptation
90+
- Same fee structure as commerce (no bypass vector)
91+
- [ ] **Donor-facing amount selection page** (`/donate/{slug}`)
92+
- Preset amount buttons (configurable per link) + custom amount input
93+
- Campaign progress bar (if goal set)
94+
- Org name + mission statement display
95+
- Min/max amount validation ($1–$10K default, configurable)
96+
- Resolves to standard checkout flow (`/pay/{id}`)
97+
- [ ] **Donation checkout UX** — conditional UI in existing checkout page
98+
- "Donation to [Org]" header instead of product name
99+
- Custom thank-you message from org (replaces generic receipt)
100+
- Suppress overpaid warning (overpayment = generosity)
101+
- Hide refund address field (donations aren't refundable)
102+
- Download proof-of-donation receipt (image export)
103+
- [ ] **Dashboard donation management** — integrated into existing tabs
104+
- Donation link creation/management in Links tab (toggle: Payment Links | Donation Links)
105+
- `donation` type filter in Invoices tab (alongside billing/recurring/payment)
106+
- Donation summary card in Overview tab (total raised, active campaigns)
107+
- [ ] **Public donation link info endpoint** (`GET /api/payment-links/{slug}/info`)
108+
- Returns donation config, campaign progress, org name (no invoice creation)
109+
- Powers amount selection page and future embeddable widget
110+
111+
## Phase 3.6 -- Charity Split (requires Phase 3.5 + wallet testing)
112+
113+
- [ ] **3-output ZIP-321 wallet testing** — verify Zashi/YWallet handle 3+ output URIs on testnet
114+
- CipherPay already uses 2-output (merchant + fee); charity adds a 3rd
115+
- Blocking test: if wallets don't support 3 outputs, this phase is deferred
116+
- [ ] **Merchant charity pledge** — dashboard setting: "Donate X% of sales to [Org]"
117+
- Third output added to ZIP-321 URI at invoice creation (merchant + fee + charity)
118+
- Org must be registered on CipherPay with a viewing key
119+
- Badge on checkout page: "This merchant supports [Org Name]"
120+
- [ ] **Donor opt-in at checkout** — "Add $1 to [Org]?" toggle on payment page
121+
- Adjusts total and adds third output to zcash URI
122+
- Org selectable from CipherPay-registered nonprofits
123+
- [ ] **Round-up for charity** — round to nearest dollar, difference goes to org
124+
- Classic retail pattern adapted to shielded payments
125+
77126
## Phase 4 -- Production Infrastructure
78127

79128
- [ ] **Encryption at rest** for UFVKs (AES-256-GCM with HSM-backed key)
@@ -146,8 +195,8 @@ Privacy-preserving Zcash payment gateway. Non-custodial, shielded-only.
146195
These are changes needed in the CipherScan explorer/indexer to support CipherPay at scale:
147196

148197
- [x] `GET /api/tx/{txid}/raw` -- raw hex endpoint for trial decryption
198+
- [x] **WebSocket mempool stream with tiered broadcast** -- service clients (authenticated via `X-Service-Key`) subscribe to `raw_mempool` channel and receive `mempool_tx` events enriched with `raw_hex`. Regular browser clients receive the slim payload. Powered by Zebra gRPC indexer (`MempoolChange` + `ChainTipChange` streams).
149199
- [ ] `POST /api/tx/raw/batch` -- batch raw hex endpoint (Phase 2)
150-
- [ ] `ws://api.cipherscan.app/mempool/stream` -- WebSocket mempool stream (Phase 2)
151200
- [ ] Multi-Zebra-node infrastructure with load balancing (Phase 4)
152201
- [ ] Rate limit tiers for API consumers (Phase 5)
153202
- [ ] Dedicated CipherPay API key with higher rate limits (Phase 5)

src/api/invoices.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,8 @@ pub async fn get(
156156
"overpaid": overpaid,
157157
"is_event": is_event,
158158
"is_luma": is_luma,
159+
"is_donation": inv.is_donation == 1,
160+
"payment_link_id": inv.payment_link_id,
159161
}))
160162
}
161163
None => HttpResponse::NotFound().json(serde_json::json!({

src/api/mod.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,9 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
9191
.route("/payment-links/{id}", web::patch().to(payment_links::update))
9292
.route("/payment-links/{id}", web::delete().to(payment_links::delete))
9393
.route("/payment-links/{slug}/checkout", web::post().to(payment_links::resolve).wrap(Governor::new(&session_rate_limit)))
94+
.route("/payment-links/{slug}/info", web::get().to(payment_links::info))
95+
// Donation links (merchant auth)
96+
.route("/donation-links", web::post().to(payment_links::create_donation))
9497
// Buyer checkout (public)
9598
.route("/checkout", web::post().to(checkout))
9699
// Invoice endpoints (API key auth)
@@ -554,7 +557,8 @@ async fn list_invoices(
554557
detected_at, expires_at, confirmed_at, refunded_at,
555558
refund_address, invoices.created_at, price_zatoshis, received_zatoshis,
556559
(EXISTS (SELECT 1 FROM events WHERE product_id = invoices.product_id)) AS is_event,
557-
pr.label AS price_label
560+
pr.label AS price_label,
561+
invoices.is_donation, invoices.payment_link_id
558562
FROM invoices
559563
LEFT JOIN prices pr ON pr.id = invoices.price_id
560564
WHERE invoices.merchant_id = ? ORDER BY invoices.created_at DESC LIMIT 50",
@@ -601,6 +605,8 @@ async fn list_invoices(
601605
"overpaid": rz > pz + 1000 && pz > 0,
602606
"is_event": r.get::<bool, _>("is_event"),
603607
"price_label": r.get::<Option<String>, _>("price_label"),
608+
"is_donation": r.get::<i32, _>("is_donation") == 1,
609+
"payment_link_id": r.get::<Option<String>, _>("payment_link_id"),
604610
})
605611
})
606612
.collect();

0 commit comments

Comments
 (0)