diff --git a/models.py b/models.py index 331ad6d..a16f7dd 100644 --- a/models.py +++ b/models.py @@ -45,6 +45,8 @@ class EventExtra(BaseModel): nostr_notifications: bool = False notification_subject: str = "" notification_body: str = "" + onchain_enabled: bool = False + onchain_wallet_id: str | None = None class CreateEvent(BaseModel): @@ -110,6 +112,7 @@ class TicketExtra(BaseModel): email_notification_sent: bool = False nostr_notification_sent: bool = False refunded: bool = False + onchain: bool = False class CreateTicket(BaseModel): @@ -167,6 +170,9 @@ class TicketPaymentRequest(BaseModel): fiat_payment_request: str | None = None fiat_provider: str | None = None is_fiat: bool = False + onchain_address: str | None = None + onchain_mempool_endpoint: str | None = None + onchain_amount_sat: int | None = None class TicketFilters(FilterModel): diff --git a/services.py b/services.py index 057026b..84c16e0 100644 --- a/services.py +++ b/services.py @@ -5,7 +5,9 @@ from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from html import escape +from typing import Any +import httpx from lnbits.core.models.users import UserNotifications from lnbits.core.services.notifications import send_user_notification from lnbits.helpers import is_valid_email_address @@ -29,6 +31,47 @@ ) +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 fetch_onchain_address(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/address/{wallet_id}", + headers={"X-API-KEY": api_key}, + ) + resp.raise_for_status() + return resp.json() + + async def set_ticket_paid(ticket: Ticket) -> Ticket: if ticket.paid: return ticket diff --git a/static/js/display.js b/static/js/display.js index 498cd2c..a5cc160 100644 --- a/static/js/display.js +++ b/static/js/display.js @@ -27,7 +27,11 @@ window.PageEventsDisplay = { show: false, status: 'pending', paymentReq: null, - isFiat: false + isFiat: false, + isOnchain: false, + onchainAddress: null, + onchainAmountSat: 0, + mempoolEndpoint: null }, paymentDismissMsg: null, paymentWebsocket: null @@ -79,6 +83,32 @@ window.PageEventsDisplay = { }, allowNostrNotifications() { return Boolean(this.event?.extra?.nostr_notifications) + }, + allowOnchain() { + return Boolean(this.event?.extra?.onchain_enabled) + }, + showPaymentMethodSelector() { + return this.allowFiatCheckout || this.allowOnchain + }, + paymentMethodOptions() { + const options = [{label: 'Lightning', value: 'lightning'}] + if (this.allowFiatCheckout) { + options.push({label: this.fiatCheckoutLabel, value: 'fiat'}) + } + if (this.allowOnchain) { + options.push({label: 'Bitcoin', value: 'onchain'}) + } + return options + }, + onchainPaymentUri() { + if (!this.receive.onchainAddress) return '' + const btc = (this.receive.onchainAmountSat / 100000000).toFixed(8) + return `bitcoin:${this.receive.onchainAddress}?amount=${btc}` + }, + mempoolAddressUrl() { + if (!this.receive.onchainAddress || !this.receive.mempoolEndpoint) + return null + return `${this.receive.mempoolEndpoint}/address/${this.receive.onchainAddress}` } }, methods: { @@ -131,7 +161,11 @@ window.PageEventsDisplay = { show: false, status: 'pending', paymentReq: null, - isFiat: false + isFiat: false, + isOnchain: false, + onchainAddress: null, + onchainAmountSat: 0, + mempoolEndpoint: null } }, nameValidation(val) { @@ -169,7 +203,11 @@ window.PageEventsDisplay = { show: false, status: 'complete', paymentReq: null, - isFiat: false + isFiat: false, + isOnchain: false, + onchainAddress: null, + onchainAmountSat: 0, + mempoolEndpoint: null } this.ticketLink = { show: true, @@ -192,15 +230,18 @@ window.PageEventsDisplay = { promo_code: this.formDialog.data.promo_code || null, refund_address: this.formDialog.data.refund || null, nostr_identifier: this.formDialog.data.nostr_identifier || null, - payment_method: this.allowFiatCheckout + payment_method: this.showPaymentMethodSelector ? this.formDialog.data.payment_method : 'lightning' } ) - const isFiat = Boolean(data.is_fiat) + const isOnchain = Boolean(data.onchain_address) + const isFiat = !isOnchain && Boolean(data.is_fiat) this.paymentReq = isFiat ? data.fiat_payment_request || null - : data.payment_request + : isOnchain + ? null + : data.payment_request this.paymentHash = data.payment_hash this.paymentDismissMsg = Quasar.Notify.create({ @@ -211,7 +252,11 @@ window.PageEventsDisplay = { show: true, status: 'pending', paymentReq: this.paymentReq, - isFiat + isFiat, + isOnchain, + onchainAddress: data.onchain_address || null, + onchainAmountSat: data.onchain_amount_sat || 0, + mempoolEndpoint: data.onchain_mempool_endpoint || null } if (isFiat && this.paymentReq) { window.open(this.paymentReq, '_blank', 'noopener') diff --git a/static/js/display.vue b/static/js/display.vue index 382f25a..d1ab493 100644 --- a/static/js/display.vue +++ b/static/js/display.vue @@ -80,20 +80,18 @@ " >