diff --git a/models.py b/models.py index ed75db8..bf7fd32 100644 --- a/models.py +++ b/models.py @@ -98,14 +98,6 @@ class EventExtra(EventExtraBase): # `sync_event_ticket_waves`), which is why fork code that reads them # needs auditing — aiolabs/events#61. ticket_waves: list[TicketWave] = Field(default_factory=list) - # Upstream's SatsPay-backed on-chain config. Removed in the commit - # that follows this merge — we use native lnbits on-chain instead - # (aiolabs/events#41). Kept here so the removal is a legible diff - # against what upstream shipped rather than an invisible omission. - onchain_enabled: bool = False - onchain_wallet_id: str | None = None - onchain_zeroconf: bool = False - onchain_fasttrack: bool = False PublicEventExtra = EventExtraBase @@ -256,9 +248,11 @@ class TicketExtra(BaseModel): email_notification_sent: bool = False nostr_notification_sent: bool = False refunded: bool = False + # On-chain vocabulary, populated once native lnbits on-chain lands + # (aiolabs/events#41). Upstream's field names, minus the SatsPay charge + # id — SatsPay is the implementation we are not adopting. onchain: bool = False onchain_address: str | None = None - satspay_charge_id: str | None = None class CreateTicket(BaseModel): @@ -368,7 +362,6 @@ class TicketPaymentRequest(BaseModel): # My Tickets. ticket_ids: list[str] = Field(default_factory=list) onchain_amount_sat: int | None = None - satspay_charge_url: str | None = None class TicketFilters(FilterModel): diff --git a/services.py b/services.py index 026121b..84d5798 100644 --- a/services.py +++ b/services.py @@ -9,9 +9,7 @@ from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from email.utils import formataddr, formatdate, make_msgid from html import escape -from typing import Any -import httpx from lnbits.core.models.users import UserNotifications from lnbits.core.services.nostr import send_nostr_dm from lnbits.core.services.notifications import send_user_notification @@ -45,59 +43,6 @@ from .qr import ( ticket_card_filename, ) - -async def fetch_watchonly_config(api_key: str) -> dict[str, Any]: - async with httpx.AsyncClient() as client: - resp = await client.get( - url=f"http://{settings.host}:{settings.port}/watchonly/api/v1/config", - headers={"X-API-KEY": api_key}, - ) - resp.raise_for_status() - return resp.json() - - -async def fetch_watchonly_wallets(api_key: str, network: str) -> list[dict[str, Any]]: - async with httpx.AsyncClient() as client: - resp = await client.get( - url=f"http://{settings.host}:{settings.port}/watchonly/api/v1/wallet", - headers={"X-API-KEY": api_key}, - params={"network": network}, - ) - resp.raise_for_status() - return resp.json() - - -async def fetch_watchonly_wallet(api_key: str, wallet_id: str) -> dict[str, Any]: - async with httpx.AsyncClient() as client: - resp = await client.get( - url=f"http://{settings.host}:{settings.port}/watchonly/api/v1/wallet/{wallet_id}", - headers={"X-API-KEY": api_key}, - ) - resp.raise_for_status() - return resp.json() - - -async def get_satspay_charge(api_key: str, charge_id: str) -> dict[str, Any]: - async with httpx.AsyncClient() as client: - resp = await client.get( - url=f"http://{settings.host}:{settings.port}/satspay/api/v1/charge/{charge_id}", - headers={"X-API-KEY": api_key}, - ) - resp.raise_for_status() - return resp.json() - - -async def create_satspay_charge(api_key: str, data: dict) -> dict[str, Any]: - async with httpx.AsyncClient() as client: - resp = await client.post( - url=f"http://{settings.host}:{settings.port}/satspay/api/v1/charge", - headers={"X-API-KEY": api_key}, - json=data, - ) - resp.raise_for_status() - return resp.json() - - DEFAULT_NOSTR_RELAYS = [ "wss://relay.damus.io", "wss://relay.primal.net", diff --git a/static/js/display.js b/static/js/display.js index 03c5f9f..022f051 100644 --- a/static/js/display.js +++ b/static/js/display.js @@ -83,19 +83,13 @@ window.PageEventsDisplay = { }, paymentMethodOptions() { // Options come from `paymentMethods` — the same list the backend - // validates against in `effective_payment_methods` — rather than being - // re-derived from per-method booleans. v1.6.8 added a second copy of - // this computed that built the list from `allow_fiat`/`onchain_enabled` - // instead; because it was defined later in the object it silently won - // the merge, and it offers a "Bitcoin" option for any event with - // `extra.onchain_enabled` even though the backend rejects `onchain` - // unless it is in `extra.payment_methods` (views_api: "not in - // effective_payment_methods"). Labels below are upstream's; the source - // of the list is ours. + // validates against in `effective_payment_methods` — rather than + // being re-derived from per-method booleans, so a rail the server + // would reject can never be offered. The `|| method` fallback covers + // a method with no label yet (on-chain, once #41 lands). const labels = { lightning: 'Lightning', - fiat: this.fiatCheckoutLabel, - onchain: 'Bitcoin' + fiat: this.fiatCheckoutLabel } return this.paymentMethods.map(method => ({ value: method, @@ -243,11 +237,6 @@ window.PageEventsDisplay = { : this.paymentMethods[0] || 'lightning' } ) - if (data.satspay_charge_url) { - window.location.href = data.satspay_charge_url - return - } - const isFiat = Boolean(data.is_fiat) this.paymentReq = isFiat ? data.fiat_payment_request || null diff --git a/static/js/index.js b/static/js/index.js index 3b4bb78..f702a4e 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -272,8 +272,7 @@ window.PageEvents = { promo_codes: [] } } - }, - onchainWallets: [] + } } }, computed: { @@ -302,42 +301,6 @@ window.PageEvents = { if (!value) return '' return value.length > 4 ? `${value.slice(0, 4)}...` : value }, - async loadOnchainWallets() { - const wallet = _.findWhere(this.g.user.wallets, { - id: this.formDialog.data.wallet - }) - if (!wallet) return - try { - const {data} = await LNbits.api.request( - 'GET', - '/events/api/v1/events/onchain/status', - wallet.adminkey - ) - this.onchainWallets = data.available ? data.wallets || [] : [] - } catch { - this.onchainWallets = [] - } - }, - async confirmOnchainTicket(ticket) { - const wallet = _.findWhere(this.g.user.wallets, {id: ticket.wallet}) - if (!wallet) return - try { - await LNbits.api.request( - 'PUT', - `/events/api/v1/tickets/${ticket.id}/onchain-confirm`, - wallet.adminkey - ) - Quasar.Notify.create({ - type: 'positive', - message: 'Onchain payment confirmed.', - icon: null - }) - await this.getTickets() - await this.getAllTickets() - } catch (error) { - LNbits.utils.notifyApiError(error) - } - }, primaryTicketWave(data = this.formDialog.data) { if (!data.extra) data.extra = {} if (!data.extra.ticket_waves || data.extra.ticket_waves.length === 0) { @@ -841,10 +804,6 @@ window.PageEvents = { min_tickets: 1, email_notifications: false, nostr_notifications: false, - onchain_enabled: false, - onchain_wallet_id: null, - onchain_zeroconf: false, - onchain_fasttrack: false, ticket_waves: [ { id: 'primary', @@ -867,12 +826,6 @@ window.PageEvents = { } } this.formDialog.show = true - if ( - this.formDialog.data.wallet && - this.formDialog.data.extra?.onchain_enabled - ) { - this.loadOnchainWallets() - } }, resetEventDialog() { this.formDialog.show = false @@ -888,8 +841,6 @@ window.PageEvents = { min_tickets: 1, email_notifications: false, nostr_notifications: false, - onchain_enabled: false, - onchain_wallet_id: null, ticket_waves: [ { id: 'primary', diff --git a/static/js/index.vue b/static/js/index.vue index 86e082a..a52d056 100644 --- a/static/js/index.vue +++ b/static/js/index.vue @@ -408,18 +408,6 @@ > Resend ticket email - - Confirm onchain payment - @@ -909,55 +897,6 @@ - -
-
- Accept Bitcoin onchain payments. Requires the - SatsPay and Watchonly - extensions to be installed and enabled. -
- - -
- - -
-
-
-
dict[str, Any]: - try: - config = await fetch_watchonly_config(wallet.inkey) - network = config.get("network") - if not network: - raise HTTPException( - status_code=HTTPStatus.BAD_REQUEST, - detail="Watchonly extension returned an invalid network.", - ) - wallets = await fetch_watchonly_wallets(wallet.inkey, network) - except HTTPException: - raise - except Exception as exc: - return { - "available": False, - "message": f"Watchonly extension is not reachable: {exc!s}", - "network": None, - "wallets": [], - "mempool_endpoint": None, - } - return { - "available": True, - "message": None, - "network": network, - "wallets": wallets, - "mempool_endpoint": config.get("mempool_endpoint"), - } - - def _is_fiat_currency(currency: str | None) -> bool: return str(currency or "").lower() not in {"sat", "sats"} @@ -294,13 +259,6 @@ async def api_get_settings_public( return {"auto_approve": settings.auto_approve} -@events_api_router.get("/onchain/status") -async def api_onchain_status( - wallet: WalletTypeInfo = Depends(require_admin_key), -) -> dict[str, Any]: - return await _get_watchonly_status(wallet.wallet) - - @events_api_router.get("/{event_id}", response_model=PublicEvent) async def api_get_event(event_id: str) -> Event: """Public event detail used by display.vue. @@ -821,7 +779,7 @@ async def api_ticket_create( refund_address = data.refund_address nostr_identifier = data.nostr_identifier.strip() if data.nostr_identifier else None payment_method = (data.payment_method or "lightning").lower() - if payment_method not in {"lightning", "fiat", "onchain"}: + if payment_method not in {"lightning", "fiat"}: raise HTTPException( status_code=HTTPStatus.BAD_REQUEST, detail="Unsupported payment method.", @@ -931,7 +889,6 @@ async def api_ticket_create( invoice_unit = selected_wave.currency fiat_amount = price fiat_provider = None - onchain_amount_sat = None if payment_method == "fiat": if _is_fiat_currency(selected_wave.currency): @@ -956,80 +913,6 @@ async def api_ticket_create( status_code=HTTPStatus.BAD_REQUEST, detail="No fiat payment provider configured for this event.", ) - elif payment_method == "onchain": - onchain_amount_sat = int(price) - wallet_record = await get_wallet(event.wallet) - if not wallet_record: - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, - detail="Event wallet does not exist.", - ) - if not event.extra.onchain_enabled: - raise HTTPException( - status_code=HTTPStatus.BAD_REQUEST, - detail="Onchain payments are not enabled for this event.", - ) - if not event.extra.onchain_wallet_id: - raise HTTPException( - status_code=HTTPStatus.BAD_REQUEST, - detail="No onchain wallet configured for this event.", - ) - - ticket_id = urlsafe_short_hash() - base_url = str(request.base_url).rstrip("/") - # Use internal address for webhook — SatsPay calls it server-side and - # cannot reach the public domain from within the container. - internal_base = f"http://{settings.host}:{settings.port}" - webhook_url = ( - f"{internal_base}/events/api/v1/tickets/{ticket_id}/satspay-webhook" - ) - complete_url = f"{base_url}/events/ticket/{ticket_id}" - try: - charge = await create_satspay_charge( - api_key=wallet_record.inkey, - data={ - "amount": onchain_amount_sat, - "description": f"Ticket for {event.name}", - "name": name, - "onchainwallet": event.extra.onchain_wallet_id, - "zeroconf": event.extra.onchain_zeroconf, - "fasttrack": event.extra.onchain_fasttrack, - "webhook": webhook_url, - "completelink": complete_url, - "completelinktext": "View your ticket", - "time": 1440, - }, - ) - except Exception as exc: - raise HTTPException( - status_code=HTTPStatus.BAD_REQUEST, - detail=f"Failed to create SatsPay charge: {exc}", - ) from exc - - await create_ticket( - payment_hash=ticket_id, - wallet=event.wallet, - event=event.id, - name=name, - email=email, - extra={ - "applied_promo_code": promo_code, - "ticket_wave_id": selected_wave.id, - "ticket_wave_title": selected_wave.title, - "refund_address": refund_address, - "nostr_identifier": nostr_identifier, - "ticket_base_url": base_url, - "sats_paid": onchain_amount_sat, - "onchain": True, - "satspay_charge_id": charge["id"], - }, - ) - - return TicketPaymentRequest( - payment_hash=ticket_id, - onchain_amount_sat=onchain_amount_sat, - satspay_charge_url=f"/satspay/{charge['id']}", - ) else: invoice_unit = "sat" @@ -1194,73 +1077,6 @@ async def api_ticket_delete( await delete_ticket(ticket_id) -@tickets_api_router.post("/{ticket_id}/satspay-webhook") -async def api_ticket_satspay_webhook(ticket_id: str) -> None: - ticket = await get_ticket(ticket_id) - if not ticket: - logger.warning(f"SatsPay webhook: ticket {ticket_id} does not exist.") - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="Ticket does not exist." - ) - if ticket.paid: - logger.warning(f"SatsPay webhook: ticket {ticket_id} already paid.") - return - if not ticket.extra.satspay_charge_id: - raise HTTPException( - status_code=HTTPStatus.BAD_REQUEST, detail="Not a SatsPay ticket." - ) - wallet = await get_wallet(ticket.wallet) - if not wallet: - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="Ticket wallet does not exist." - ) - try: - charge = await get_satspay_charge(wallet.inkey, ticket.extra.satspay_charge_id) - except Exception as exc: - raise HTTPException( - status_code=HTTPStatus.BAD_REQUEST, detail=f"Could not verify charge: {exc}" - ) from exc - if not charge.get("paid"): - logger.warning( - f"SatsPay webhook for ticket {ticket_id}: charge" - f" {ticket.extra.satspay_charge_id} not paid." - ) - return - - ticket = await set_ticket_paid(ticket) - send_ticket_notification_in_background(ticket) - for queue in payment_listeners.get(ticket_id, []): - queue.put_nowait(ticket) - - -@tickets_api_router.put("/{payment_hash}/onchain-confirm") -async def api_ticket_onchain_confirm( - payment_hash: str, - wallet: WalletTypeInfo = Depends(require_admin_key), -) -> Ticket: - ticket = await get_ticket(payment_hash) - if not ticket: - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="Ticket does not exist." - ) - if ticket.wallet != wallet.wallet.id: - raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your ticket.") - if ticket.paid: - raise HTTPException( - status_code=HTTPStatus.BAD_REQUEST, detail="Ticket already paid." - ) - if not ticket.extra.onchain: - raise HTTPException( - status_code=HTTPStatus.BAD_REQUEST, - detail="Ticket is not an onchain payment.", - ) - ticket = await set_ticket_paid(ticket) - send_ticket_notification_in_background(ticket) - for queue in payment_listeners.get(payment_hash, []): - queue.put_nowait(ticket) - return ticket - - @tickets_api_router.post("/{ticket_id}/resend-email", response_model=TicketResendResult) async def api_ticket_resend_email( ticket_id: str,