diff --git a/app/backend/app.py b/app/backend/app.py index 82fe59a..78c294e 100644 --- a/app/backend/app.py +++ b/app/backend/app.py @@ -195,6 +195,19 @@ def get_public_lnurl_address(): return (cfg.get("public_lnurl_address") or "").strip() or PUBLIC_LNURL_ADDRESS +def get_payment_mode(): + cfg = load_config() + mode = str(cfg.get("payment_mode", "compatibility")).strip().lower() + + if mode not in ("compatibility", "privacy"): + return "compatibility" + + return mode + + +def privacy_mode_enabled(): + return get_payment_mode() == "privacy" + def _mask_secret(value: str) -> str: value = (value or "").strip() @@ -1925,13 +1938,20 @@ def lnurl_for_address(address: str) -> LnurlInfoResponse: @app.get("/.well-known/lnurlp/{username}", response_model=LnurlPayMetadataResponse) def lnurl_pay_metadata(username: str) -> LnurlPayMetadataResponse: - alias = _resolve_lnurl_alias(username) + if privacy_mode_enabled(): + raise HTTPException( + status_code=404, + detail="LNURL disabled by privacy mode", + ) + + alias = _resolve_lnurl_alias(username) min_sendable = LNURL_MIN_SENDABLE_MSAT max_sendable = LNURL_MAX_SENDABLE_MSAT if alias["fixed_amount_sat"] is not None: fixed_msat = int(alias["fixed_amount_sat"]) * 1000 + min_sendable = fixed_msat max_sendable = fixed_msat @@ -1954,6 +1974,13 @@ async def lnurl_callback( nostr: Optional[str] = Query(default=None), lnurl: Optional[str] = Query(default=None), ) -> dict[str, Any]: + + if privacy_mode_enabled(): + raise HTTPException( + status_code=404, + detail="LNURL disabled by privacy mode", + ) + alias = _resolve_lnurl_alias(username) min_sendable = LNURL_MIN_SENDABLE_MSAT @@ -2096,6 +2123,15 @@ def set_setup_config(payload: dict, request: StarletteRequest): cfg["lnurl_base_domain"] = str(safe_payload.get("lnurl_base_domain", "")).strip().lower() cfg["lnurl_base_url"] = str(safe_payload.get("lnurl_base_url", "")).strip().rstrip("/") + payment_mode = str( + safe_payload.get("payment_mode", "compatibility") + ).strip().lower() + + if payment_mode not in ("compatibility", "privacy"): + payment_mode = "compatibility" + + cfg["payment_mode"] = payment_mode + dns_mode = str(safe_payload.get("dns_mode", "none")).strip().lower() cfg["dns_mode"] = dns_mode @@ -2586,25 +2622,28 @@ async def public_index_page(): aliases = cfg.get("aliases", {}) or {} items_html = "" + for alias_name, alias in aliases.items(): description = alias.get("description") or "Lightning payment" amount_sat = alias.get("amount_sat") amount_label = f"{amount_sat} sats" if amount_sat else "variable amount" + bolt12_domain = get_public_bolt12_address().split("@", 1)[1] + bip353_address = f"{alias_name}@{bolt12_domain}" + items_html += f"""
-
{alias_name}@{get_lnurl_base_domain()}
+
{bip353_address}
{description}
Amount: {amount_label}
- +
""" - if not items_html: items_html = """
@@ -3528,7 +3567,10 @@ async def public_alias_page(alias_name: str): alias = aliases[alias_name] + privacy_mode = privacy_mode_enabled() + bip353_domain = get_bip353_base_domain() + lnurl_domain = get_lnurl_base_domain() bip353_address = f"{alias_name}@{bip353_domain}" @@ -3559,6 +3601,11 @@ async def public_alias_page(alias_name: str): except Exception: bolt11_invoice = None + subline = ( + "BOLT12 only" + if privacy_mode + else "BOLT12 first · LNURL and BOLT11 as fallback" + ) offer_section = "" if last_offer: offer_section = f""" @@ -3577,9 +3624,8 @@ async def public_alias_page(alias_name: str):
""" - bolt11_section = "" - if bolt11_invoice: + if bolt11_invoice and not privacy_mode: bolt11_section = f"""
BOLT11 Compatibility Fallback
@@ -3598,7 +3644,7 @@ async def public_alias_page(alias_name: str): """ lnurl_section = "" - if lnurl_fallback: + if lnurl_fallback and not privacy_mode: lnurl_section = f"""
LNURL Fallback
@@ -3837,7 +3883,7 @@ async def public_alias_page(alias_name: str):
{description}
Amount: {amount_label}
-BOLT12 first · LNURL and BOLT11 as fallback +{subline}
{offer_section} diff --git a/app/backend/config.py b/app/backend/config.py index cde16cb..4214f4c 100644 --- a/app/backend/config.py +++ b/app/backend/config.py @@ -10,6 +10,7 @@ "public_lnurl_address": "lnurl@pay.local", "lnurl_base_domain": "", "lnurl_base_url": "", + "payment_mode": "compatibility", # compatibility | privacy "dns_mode": "none", # none | manual | cloudflare "ui_password_hash": "", "cloudflare": { diff --git a/app/frontend/public/index.html b/app/frontend/public/index.html index dd28b27..d0baaa8 100644 --- a/app/frontend/public/index.html +++ b/app/frontend/public/index.html @@ -316,6 +316,58 @@ margin-top: 16px; } + .paymentModeLabel { + margin-top: 14px; + margin-bottom: 6px; + font-size: 1.05rem; + font-weight: 700; + } + .paymentModeCards { + display: grid; + gap: 12px; + margin-top: 12px; + } + + .paymentModeCard { + position: relative; + display: block; + padding: 20px 16px 20px 52px; + border: 1px solid var(--border); + border-radius: 14px; + cursor: pointer; + background: rgba(255,255,255,0.02); + } + + .paymentModeCard:hover { + border-color: var(--accent); + } + + .paymentModeCard input[type="radio"] { + position: absolute; + left: 18px; + top: 22px; + width: auto; + min-width: 0; + height: auto; + margin: 0; + padding: 0; + transform: scale(1.15); + } + .paymentModeTitle { + font-weight: 700; + font-size: 1rem; + } + + .paymentModeDesc { + margin-top: 4px; + color: var(--text); + } + + .paymentModeHint { + margin-top: 6px; + color: var(--muted); + font-size: 0.9rem; + } .aliasItem { border: 1px solid var(--border); background: #0c1322; @@ -603,14 +655,78 @@

⚙️ BOLT12 Pay Setup

- - +
+ Choose Payment Mode +
- - +
+ Select between maximum wallet compatibility or maximum privacy. +
+ +
+ + + + +
+
+ + + - - + + + + + + +
@@ -877,6 +993,7 @@

⚡ BOLT12 Offer & Alias Console const saveSetupBtn = document.getElementById('saveSetupBtn'); const manualDnsHint = document.getElementById('manualDnsHint'); const manualDnsExampleName = document.getElementById('manualDnsExampleName'); + const lnurlSettingsBlock = document.getElementById('lnurlSettingsBlock'); const cloudflareFields = document.getElementById('cloudflareFields'); const cfZoneName = document.getElementById('cfZoneName'); @@ -884,6 +1001,7 @@

⚡ BOLT12 Offer & Alias Console const cfApiToken = document.getElementById('cfApiToken'); const setupPassword = document.getElementById('setupPassword'); const setupPasswordConfirm = document.getElementById('setupPasswordConfirm'); + const paymentModeInputs = document.querySelectorAll('input[name="paymentMode"]'); const aliasNameInput = document.getElementById('aliasNameInput'); const aliasDescriptionInput = document.getElementById('aliasDescriptionInput'); @@ -1099,6 +1217,15 @@

⚡ BOLT12 Offer & Alias Console cloudflareFields.hidden = setupDnsMode.value !== 'cloudflare'; } + function updatePaymentModeUi() { + const privacy = + document.querySelector( + 'input[name="paymentMode"]:checked' + )?.value === 'privacy'; + + lnurlSettingsBlock.hidden = privacy; + } + // ------------------------------------------------------------------ // Data loading helpers // ------------------------------------------------------------------ @@ -1140,6 +1267,14 @@

⚡ BOLT12 Offer & Alias Console console.log('Setup config unavailable', e); } setupBolt12Address.value = configData.public_bolt12_address || ''; + + const paymentMode = + configData.payment_mode || 'compatibility'; + + paymentModeInputs.forEach(el => { + el.checked = el.value === paymentMode; + }); + updatePaymentModeUi(); setupLnurlAddress.value = configData.public_lnurl_address || ''; setupLnurlBaseDomain.value = configData.lnurl_base_domain || ''; setupLnurlBaseUrl.value = configData.lnurl_base_url || ''; @@ -1175,6 +1310,10 @@

⚡ BOLT12 Offer & Alias Console const payload = { public_bolt12_address: (setupBolt12Address.value || '').trim(), + payment_mode: + document.querySelector( + 'input[name="paymentMode"]:checked' + )?.value || 'compatibility', public_lnurl_address: (setupLnurlAddress.value || '').trim(), lnurl_base_domain: (setupLnurlBaseDomain.value || '').trim().toLowerCase(), lnurl_base_url: (setupLnurlBaseUrl.value || '').trim(), @@ -1953,6 +2092,9 @@

⚡ BOLT12 Offer & Alias Console }); saveSetupBtn.addEventListener('click', saveSetup); + paymentModeInputs.forEach(el => { + el.addEventListener('change', updatePaymentModeUi); + }); setupDnsMode.addEventListener('change', updateDnsModeUi); setupLnurlBaseDomain.addEventListener('input', updateDnsModeUi); setupBolt12Address.addEventListener('input', updateDnsModeUi); @@ -2170,6 +2312,17 @@

⚡ BOLT12 Offer & Alias Console "setup.onionWarningUmbrelFiles": "Öffne Umbrel → Files und navigiere zu:", "setup.onionWarningStartOS": "Keine manuelle Konfiguration erforderlich. Falls Onion Messages nicht aktiviert sind, fordert BOLT12 Pay automatisch die benötigte LND Auto-Configure-Aufgabe an.", "setup.copyUmbrelConfig": "Umbrel-Konfiguration kopieren", + "setup.paymentModeTitle": "Kompatibilität oder Privatsphäre?", + "setup.paymentModeHint": "Wähle zwischen maximaler Wallet-Kompatibilität und maximaler Privatsphäre.", + + "setup.paymentMode.compatibility.title": "Kompatibilität", + "setup.paymentMode.compatibility.desc": "BOLT12 + LNURL + BOLT11 Fallback", + "setup.paymentMode.compatibility.hint": "Beste Kompatibilität mit heutigen Wallets.", + + "setup.paymentMode.privacy.title": "Privatsphäre", + "setup.paymentMode.privacy.desc": "Nur BOLT12 + BIP353", + "setup.paymentMode.privacy.hint": "Keine LNURL-Endpunkte werden veröffentlicht.", + "dns.none": "Keine", "dns.manual": "Manuell", "dns.cloudflare": "Cloudflare", @@ -2269,6 +2422,16 @@

⚡ BOLT12 Offer & Alias Console "setup.onionWarningStartOS": "No manual configuration required. If Onion Messages are not enabled, BOLT12 Pay will automatically request the required LND Auto-Configure task.", "setup.onionWarningUmbrelFiles": "Open Umbrel → Files and navigate to:", "setup.copyUmbrelConfig": "Copy Umbrel config", + "setup.paymentModeTitle": "Compatibility or Privacy?", + "setup.paymentModeHint": "Choose between maximum wallet compatibility and maximum privacy.", + + "setup.paymentMode.compatibility.title": "Compatibility", + "setup.paymentMode.compatibility.desc": "BOLT12 + LNURL + BOLT11 fallback", + "setup.paymentMode.compatibility.hint": "Best compatibility with today's wallets.", + + "setup.paymentMode.privacy.title": "Privacy", + "setup.paymentMode.privacy.desc": "BOLT12 + BIP353 only", + "setup.paymentMode.privacy.hint": "No LNURL endpoints are exposed.", "dns.none": "None", "dns.manual": "Manual", "dns.cloudflare": "Cloudflare",