refactor: drop the SatsPay/watchonly on-chain surface
v1.6.8 sells tickets on-chain through SatsPay charges — a per-purchase watchonly address, a hosted charge page, and a webhook back into the extension. We are not adopting that: on-chain goes through the fork's native LndRestWallet support once core can mint purpose-bound receiving addresses (aiolabs/lnbits#53). This is the parity note on #41, applied as its own commit so the merge it follows stays a faithful diff of what upstream shipped. Removed: - services: the five SatsPay/watchonly HTTP clients (and with them the only uses of httpx and typing.Any in that module) - views_api: _get_watchonly_status, GET /events/onchain/status, the SatsPay charge branch of api_ticket_create, POST /tickets/{id}/satspay-webhook, PUT /tickets/{hash}/onchain-confirm - models: EventExtra.onchain_{enabled,wallet_id,zeroconf,fasttrack}, TicketExtra.satspay_charge_id, TicketPaymentRequest.satspay_charge_url - frontend: the organiser's "Onchain payments" panel and its watchonly wallet picker, the per-ticket confirm-onchain button, the SatsPay charge-page redirect, and the wallet-status fetch behind them `onchain` is no longer an accepted payment_method — there is no rail to fulfil it until #41 lands, so accepting it could only fail later. Kept as vocabulary for #41, in upstream's field names so it needs no migration: TicketExtra.onchain / onchain_address and TicketPaymentRequest.onchain_amount_sat. 35 routes register with no duplicates; ruff, black, prettier clean; 97 tests pass; mypy error set still identical to HEAD's baseline.
This commit is contained in:
parent
ae5affa44f
commit
d90a0f8322
6 changed files with 10 additions and 377 deletions
186
views_api.py
186
views_api.py
|
|
@ -38,7 +38,6 @@ from lnbits.utils.exchange_rates import (
|
|||
satoshis_amount_as_fiat,
|
||||
)
|
||||
from lnbits.utils.nostr import normalize_public_key
|
||||
from loguru import logger
|
||||
from PIL import Image
|
||||
|
||||
from .crud import (
|
||||
|
|
@ -99,11 +98,7 @@ from .qr import (
|
|||
ticket_card_filename,
|
||||
)
|
||||
from .services import (
|
||||
create_satspay_charge,
|
||||
event_promo_usage,
|
||||
fetch_watchonly_config,
|
||||
fetch_watchonly_wallets,
|
||||
get_satspay_charge,
|
||||
hydrate_promo_usage,
|
||||
refund_tickets,
|
||||
resend_ticket_email_notification,
|
||||
|
|
@ -112,7 +107,6 @@ from .services import (
|
|||
)
|
||||
from .tasks import (
|
||||
deregister_payment_listener,
|
||||
payment_listeners,
|
||||
register_payment_listener,
|
||||
)
|
||||
|
||||
|
|
@ -123,35 +117,6 @@ promo_api_router = APIRouter(prefix="/api/v1/promo")
|
|||
tickets_filters = parse_filters(TicketFilters)
|
||||
|
||||
|
||||
async def _get_watchonly_status(wallet) -> 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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue