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
13
models.py
13
models.py
|
|
@ -98,14 +98,6 @@ class EventExtra(EventExtraBase):
|
||||||
# `sync_event_ticket_waves`), which is why fork code that reads them
|
# `sync_event_ticket_waves`), which is why fork code that reads them
|
||||||
# needs auditing — aiolabs/events#61.
|
# needs auditing — aiolabs/events#61.
|
||||||
ticket_waves: list[TicketWave] = Field(default_factory=list)
|
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
|
PublicEventExtra = EventExtraBase
|
||||||
|
|
@ -256,9 +248,11 @@ class TicketExtra(BaseModel):
|
||||||
email_notification_sent: bool = False
|
email_notification_sent: bool = False
|
||||||
nostr_notification_sent: bool = False
|
nostr_notification_sent: bool = False
|
||||||
refunded: 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: bool = False
|
||||||
onchain_address: str | None = None
|
onchain_address: str | None = None
|
||||||
satspay_charge_id: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class CreateTicket(BaseModel):
|
class CreateTicket(BaseModel):
|
||||||
|
|
@ -368,7 +362,6 @@ class TicketPaymentRequest(BaseModel):
|
||||||
# My Tickets.
|
# My Tickets.
|
||||||
ticket_ids: list[str] = Field(default_factory=list)
|
ticket_ids: list[str] = Field(default_factory=list)
|
||||||
onchain_amount_sat: int | None = None
|
onchain_amount_sat: int | None = None
|
||||||
satspay_charge_url: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class TicketFilters(FilterModel):
|
class TicketFilters(FilterModel):
|
||||||
|
|
|
||||||
55
services.py
55
services.py
|
|
@ -9,9 +9,7 @@ from email.mime.multipart import MIMEMultipart
|
||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
from email.utils import formataddr, formatdate, make_msgid
|
from email.utils import formataddr, formatdate, make_msgid
|
||||||
from html import escape
|
from html import escape
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
import httpx
|
|
||||||
from lnbits.core.models.users import UserNotifications
|
from lnbits.core.models.users import UserNotifications
|
||||||
from lnbits.core.services.nostr import send_nostr_dm
|
from lnbits.core.services.nostr import send_nostr_dm
|
||||||
from lnbits.core.services.notifications import send_user_notification
|
from lnbits.core.services.notifications import send_user_notification
|
||||||
|
|
@ -45,59 +43,6 @@ from .qr import (
|
||||||
ticket_card_filename,
|
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 = [
|
DEFAULT_NOSTR_RELAYS = [
|
||||||
"wss://relay.damus.io",
|
"wss://relay.damus.io",
|
||||||
"wss://relay.primal.net",
|
"wss://relay.primal.net",
|
||||||
|
|
|
||||||
|
|
@ -83,19 +83,13 @@ window.PageEventsDisplay = {
|
||||||
},
|
},
|
||||||
paymentMethodOptions() {
|
paymentMethodOptions() {
|
||||||
// Options come from `paymentMethods` — the same list the backend
|
// Options come from `paymentMethods` — the same list the backend
|
||||||
// validates against in `effective_payment_methods` — rather than being
|
// validates against in `effective_payment_methods` — rather than
|
||||||
// re-derived from per-method booleans. v1.6.8 added a second copy of
|
// being re-derived from per-method booleans, so a rail the server
|
||||||
// this computed that built the list from `allow_fiat`/`onchain_enabled`
|
// would reject can never be offered. The `|| method` fallback covers
|
||||||
// instead; because it was defined later in the object it silently won
|
// a method with no label yet (on-chain, once #41 lands).
|
||||||
// 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.
|
|
||||||
const labels = {
|
const labels = {
|
||||||
lightning: 'Lightning',
|
lightning: 'Lightning',
|
||||||
fiat: this.fiatCheckoutLabel,
|
fiat: this.fiatCheckoutLabel
|
||||||
onchain: 'Bitcoin'
|
|
||||||
}
|
}
|
||||||
return this.paymentMethods.map(method => ({
|
return this.paymentMethods.map(method => ({
|
||||||
value: method,
|
value: method,
|
||||||
|
|
@ -243,11 +237,6 @@ window.PageEventsDisplay = {
|
||||||
: this.paymentMethods[0] || 'lightning'
|
: this.paymentMethods[0] || 'lightning'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if (data.satspay_charge_url) {
|
|
||||||
window.location.href = data.satspay_charge_url
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const isFiat = Boolean(data.is_fiat)
|
const isFiat = Boolean(data.is_fiat)
|
||||||
this.paymentReq = isFiat
|
this.paymentReq = isFiat
|
||||||
? data.fiat_payment_request || null
|
? data.fiat_payment_request || null
|
||||||
|
|
|
||||||
|
|
@ -272,8 +272,7 @@ window.PageEvents = {
|
||||||
promo_codes: []
|
promo_codes: []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
onchainWallets: []
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
@ -302,42 +301,6 @@ window.PageEvents = {
|
||||||
if (!value) return ''
|
if (!value) return ''
|
||||||
return value.length > 4 ? `${value.slice(0, 4)}...` : value
|
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) {
|
primaryTicketWave(data = this.formDialog.data) {
|
||||||
if (!data.extra) data.extra = {}
|
if (!data.extra) data.extra = {}
|
||||||
if (!data.extra.ticket_waves || data.extra.ticket_waves.length === 0) {
|
if (!data.extra.ticket_waves || data.extra.ticket_waves.length === 0) {
|
||||||
|
|
@ -841,10 +804,6 @@ window.PageEvents = {
|
||||||
min_tickets: 1,
|
min_tickets: 1,
|
||||||
email_notifications: false,
|
email_notifications: false,
|
||||||
nostr_notifications: false,
|
nostr_notifications: false,
|
||||||
onchain_enabled: false,
|
|
||||||
onchain_wallet_id: null,
|
|
||||||
onchain_zeroconf: false,
|
|
||||||
onchain_fasttrack: false,
|
|
||||||
ticket_waves: [
|
ticket_waves: [
|
||||||
{
|
{
|
||||||
id: 'primary',
|
id: 'primary',
|
||||||
|
|
@ -867,12 +826,6 @@ window.PageEvents = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.formDialog.show = true
|
this.formDialog.show = true
|
||||||
if (
|
|
||||||
this.formDialog.data.wallet &&
|
|
||||||
this.formDialog.data.extra?.onchain_enabled
|
|
||||||
) {
|
|
||||||
this.loadOnchainWallets()
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
resetEventDialog() {
|
resetEventDialog() {
|
||||||
this.formDialog.show = false
|
this.formDialog.show = false
|
||||||
|
|
@ -888,8 +841,6 @@ window.PageEvents = {
|
||||||
min_tickets: 1,
|
min_tickets: 1,
|
||||||
email_notifications: false,
|
email_notifications: false,
|
||||||
nostr_notifications: false,
|
nostr_notifications: false,
|
||||||
onchain_enabled: false,
|
|
||||||
onchain_wallet_id: null,
|
|
||||||
ticket_waves: [
|
ticket_waves: [
|
||||||
{
|
{
|
||||||
id: 'primary',
|
id: 'primary',
|
||||||
|
|
|
||||||
|
|
@ -408,18 +408,6 @@
|
||||||
>
|
>
|
||||||
<q-tooltip>Resend ticket email</q-tooltip>
|
<q-tooltip>Resend ticket email</q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
<q-btn
|
|
||||||
v-if="props.row.extra?.onchain && !props.row.paid"
|
|
||||||
flat
|
|
||||||
dense
|
|
||||||
size="xs"
|
|
||||||
@click="confirmOnchainTicket(props.row)"
|
|
||||||
icon="currency_bitcoin"
|
|
||||||
color="orange"
|
|
||||||
class="q-ml-xs"
|
|
||||||
>
|
|
||||||
<q-tooltip>Confirm onchain payment</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
</q-td>
|
</q-td>
|
||||||
|
|
||||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||||
|
|
@ -909,55 +897,6 @@
|
||||||
</div>
|
</div>
|
||||||
</q-expansion-item>
|
</q-expansion-item>
|
||||||
|
|
||||||
<q-expansion-item
|
|
||||||
group="advanced"
|
|
||||||
icon="view_in_ar"
|
|
||||||
label="Onchain payments"
|
|
||||||
>
|
|
||||||
<div class="q-mt-lg">
|
|
||||||
<div class="text-caption q-mb-md">
|
|
||||||
Accept Bitcoin onchain payments. Requires the
|
|
||||||
<strong>SatsPay</strong> and <strong>Watchonly</strong>
|
|
||||||
extensions to be installed and enabled.
|
|
||||||
</div>
|
|
||||||
<q-toggle
|
|
||||||
v-model="formDialog.data.extra.onchain_enabled"
|
|
||||||
label="Accept onchain payments"
|
|
||||||
left-label
|
|
||||||
@update:model-value="val => val && loadOnchainWallets()"
|
|
||||||
></q-toggle>
|
|
||||||
<q-select
|
|
||||||
v-if="formDialog.data.extra.onchain_enabled"
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
class="q-mt-md"
|
|
||||||
v-model="formDialog.data.extra.onchain_wallet_id"
|
|
||||||
label="Watchonly wallet"
|
|
||||||
:options="
|
|
||||||
onchainWallets.map(w => ({
|
|
||||||
label: w.title || w.id,
|
|
||||||
value: w.id
|
|
||||||
}))
|
|
||||||
"
|
|
||||||
emit-value
|
|
||||||
map-options
|
|
||||||
hint="Bitcoin watchonly wallet for receiving onchain payments"
|
|
||||||
></q-select>
|
|
||||||
<div v-if="formDialog.data.extra.onchain_enabled" class="q-mt-md">
|
|
||||||
<q-toggle
|
|
||||||
v-model="formDialog.data.extra.onchain_zeroconf"
|
|
||||||
label="Zero-conf (accept unconfirmed transactions)"
|
|
||||||
left-label
|
|
||||||
></q-toggle>
|
|
||||||
<q-toggle
|
|
||||||
v-model="formDialog.data.extra.onchain_fasttrack"
|
|
||||||
label="Fasttrack (treat pending as paid)"
|
|
||||||
left-label
|
|
||||||
></q-toggle>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-expansion-item>
|
|
||||||
|
|
||||||
<div class="row q-mt-lg">
|
<div class="row q-mt-lg">
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="formDialog.data.id"
|
v-if="formDialog.data.id"
|
||||||
|
|
|
||||||
186
views_api.py
186
views_api.py
|
|
@ -38,7 +38,6 @@ from lnbits.utils.exchange_rates import (
|
||||||
satoshis_amount_as_fiat,
|
satoshis_amount_as_fiat,
|
||||||
)
|
)
|
||||||
from lnbits.utils.nostr import normalize_public_key
|
from lnbits.utils.nostr import normalize_public_key
|
||||||
from loguru import logger
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
from .crud import (
|
from .crud import (
|
||||||
|
|
@ -99,11 +98,7 @@ from .qr import (
|
||||||
ticket_card_filename,
|
ticket_card_filename,
|
||||||
)
|
)
|
||||||
from .services import (
|
from .services import (
|
||||||
create_satspay_charge,
|
|
||||||
event_promo_usage,
|
event_promo_usage,
|
||||||
fetch_watchonly_config,
|
|
||||||
fetch_watchonly_wallets,
|
|
||||||
get_satspay_charge,
|
|
||||||
hydrate_promo_usage,
|
hydrate_promo_usage,
|
||||||
refund_tickets,
|
refund_tickets,
|
||||||
resend_ticket_email_notification,
|
resend_ticket_email_notification,
|
||||||
|
|
@ -112,7 +107,6 @@ from .services import (
|
||||||
)
|
)
|
||||||
from .tasks import (
|
from .tasks import (
|
||||||
deregister_payment_listener,
|
deregister_payment_listener,
|
||||||
payment_listeners,
|
|
||||||
register_payment_listener,
|
register_payment_listener,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -123,35 +117,6 @@ promo_api_router = APIRouter(prefix="/api/v1/promo")
|
||||||
tickets_filters = parse_filters(TicketFilters)
|
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:
|
def _is_fiat_currency(currency: str | None) -> bool:
|
||||||
return str(currency or "").lower() not in {"sat", "sats"}
|
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}
|
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)
|
@events_api_router.get("/{event_id}", response_model=PublicEvent)
|
||||||
async def api_get_event(event_id: str) -> Event:
|
async def api_get_event(event_id: str) -> Event:
|
||||||
"""Public event detail used by display.vue.
|
"""Public event detail used by display.vue.
|
||||||
|
|
@ -821,7 +779,7 @@ async def api_ticket_create(
|
||||||
refund_address = data.refund_address
|
refund_address = data.refund_address
|
||||||
nostr_identifier = data.nostr_identifier.strip() if data.nostr_identifier else None
|
nostr_identifier = data.nostr_identifier.strip() if data.nostr_identifier else None
|
||||||
payment_method = (data.payment_method or "lightning").lower()
|
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(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
detail="Unsupported payment method.",
|
detail="Unsupported payment method.",
|
||||||
|
|
@ -931,7 +889,6 @@ async def api_ticket_create(
|
||||||
invoice_unit = selected_wave.currency
|
invoice_unit = selected_wave.currency
|
||||||
fiat_amount = price
|
fiat_amount = price
|
||||||
fiat_provider = None
|
fiat_provider = None
|
||||||
onchain_amount_sat = None
|
|
||||||
|
|
||||||
if payment_method == "fiat":
|
if payment_method == "fiat":
|
||||||
if _is_fiat_currency(selected_wave.currency):
|
if _is_fiat_currency(selected_wave.currency):
|
||||||
|
|
@ -956,80 +913,6 @@ async def api_ticket_create(
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
detail="No fiat payment provider configured for this event.",
|
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:
|
else:
|
||||||
invoice_unit = "sat"
|
invoice_unit = "sat"
|
||||||
|
|
||||||
|
|
@ -1194,73 +1077,6 @@ async def api_ticket_delete(
|
||||||
await delete_ticket(ticket_id)
|
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)
|
@tickets_api_router.post("/{ticket_id}/resend-email", response_model=TicketResendResult)
|
||||||
async def api_ticket_resend_email(
|
async def api_ticket_resend_email(
|
||||||
ticket_id: str,
|
ticket_id: str,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue