parent
35c20eda72
commit
e0ea0e30e7
7 changed files with 423 additions and 33 deletions
|
|
@ -45,6 +45,8 @@ class EventExtra(BaseModel):
|
||||||
nostr_notifications: bool = False
|
nostr_notifications: bool = False
|
||||||
notification_subject: str = ""
|
notification_subject: str = ""
|
||||||
notification_body: str = ""
|
notification_body: str = ""
|
||||||
|
onchain_enabled: bool = False
|
||||||
|
onchain_wallet_id: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class CreateEvent(BaseModel):
|
class CreateEvent(BaseModel):
|
||||||
|
|
@ -110,6 +112,7 @@ 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
|
||||||
|
onchain: bool = False
|
||||||
|
|
||||||
|
|
||||||
class CreateTicket(BaseModel):
|
class CreateTicket(BaseModel):
|
||||||
|
|
@ -167,6 +170,9 @@ class TicketPaymentRequest(BaseModel):
|
||||||
fiat_payment_request: str | None = None
|
fiat_payment_request: str | None = None
|
||||||
fiat_provider: str | None = None
|
fiat_provider: str | None = None
|
||||||
is_fiat: bool = False
|
is_fiat: bool = False
|
||||||
|
onchain_address: str | None = None
|
||||||
|
onchain_mempool_endpoint: str | None = None
|
||||||
|
onchain_amount_sat: int | None = None
|
||||||
|
|
||||||
|
|
||||||
class TicketFilters(FilterModel):
|
class TicketFilters(FilterModel):
|
||||||
|
|
|
||||||
43
services.py
43
services.py
|
|
@ -5,7 +5,9 @@ from asyncio.tasks import create_task
|
||||||
from email.mime.multipart import MIMEMultipart
|
from email.mime.multipart import MIMEMultipart
|
||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
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.notifications import send_user_notification
|
from lnbits.core.services.notifications import send_user_notification
|
||||||
from lnbits.helpers import is_valid_email_address
|
from lnbits.helpers import is_valid_email_address
|
||||||
|
|
@ -29,6 +31,47 @@ from .models import (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
async def set_ticket_paid(ticket: Ticket) -> Ticket:
|
||||||
if ticket.paid:
|
if ticket.paid:
|
||||||
return ticket
|
return ticket
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,11 @@ window.PageEventsDisplay = {
|
||||||
show: false,
|
show: false,
|
||||||
status: 'pending',
|
status: 'pending',
|
||||||
paymentReq: null,
|
paymentReq: null,
|
||||||
isFiat: false
|
isFiat: false,
|
||||||
|
isOnchain: false,
|
||||||
|
onchainAddress: null,
|
||||||
|
onchainAmountSat: 0,
|
||||||
|
mempoolEndpoint: null
|
||||||
},
|
},
|
||||||
paymentDismissMsg: null,
|
paymentDismissMsg: null,
|
||||||
paymentWebsocket: null
|
paymentWebsocket: null
|
||||||
|
|
@ -79,6 +83,32 @@ window.PageEventsDisplay = {
|
||||||
},
|
},
|
||||||
allowNostrNotifications() {
|
allowNostrNotifications() {
|
||||||
return Boolean(this.event?.extra?.nostr_notifications)
|
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: {
|
methods: {
|
||||||
|
|
@ -131,7 +161,11 @@ window.PageEventsDisplay = {
|
||||||
show: false,
|
show: false,
|
||||||
status: 'pending',
|
status: 'pending',
|
||||||
paymentReq: null,
|
paymentReq: null,
|
||||||
isFiat: false
|
isFiat: false,
|
||||||
|
isOnchain: false,
|
||||||
|
onchainAddress: null,
|
||||||
|
onchainAmountSat: 0,
|
||||||
|
mempoolEndpoint: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
nameValidation(val) {
|
nameValidation(val) {
|
||||||
|
|
@ -169,7 +203,11 @@ window.PageEventsDisplay = {
|
||||||
show: false,
|
show: false,
|
||||||
status: 'complete',
|
status: 'complete',
|
||||||
paymentReq: null,
|
paymentReq: null,
|
||||||
isFiat: false
|
isFiat: false,
|
||||||
|
isOnchain: false,
|
||||||
|
onchainAddress: null,
|
||||||
|
onchainAmountSat: 0,
|
||||||
|
mempoolEndpoint: null
|
||||||
}
|
}
|
||||||
this.ticketLink = {
|
this.ticketLink = {
|
||||||
show: true,
|
show: true,
|
||||||
|
|
@ -192,14 +230,17 @@ window.PageEventsDisplay = {
|
||||||
promo_code: this.formDialog.data.promo_code || null,
|
promo_code: this.formDialog.data.promo_code || null,
|
||||||
refund_address: this.formDialog.data.refund || null,
|
refund_address: this.formDialog.data.refund || null,
|
||||||
nostr_identifier: this.formDialog.data.nostr_identifier || null,
|
nostr_identifier: this.formDialog.data.nostr_identifier || null,
|
||||||
payment_method: this.allowFiatCheckout
|
payment_method: this.showPaymentMethodSelector
|
||||||
? this.formDialog.data.payment_method
|
? this.formDialog.data.payment_method
|
||||||
: 'lightning'
|
: 'lightning'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
const isFiat = Boolean(data.is_fiat)
|
const isOnchain = Boolean(data.onchain_address)
|
||||||
|
const isFiat = !isOnchain && Boolean(data.is_fiat)
|
||||||
this.paymentReq = isFiat
|
this.paymentReq = isFiat
|
||||||
? data.fiat_payment_request || null
|
? data.fiat_payment_request || null
|
||||||
|
: isOnchain
|
||||||
|
? null
|
||||||
: data.payment_request
|
: data.payment_request
|
||||||
this.paymentHash = data.payment_hash
|
this.paymentHash = data.payment_hash
|
||||||
|
|
||||||
|
|
@ -211,7 +252,11 @@ window.PageEventsDisplay = {
|
||||||
show: true,
|
show: true,
|
||||||
status: 'pending',
|
status: 'pending',
|
||||||
paymentReq: this.paymentReq,
|
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) {
|
if (isFiat && this.paymentReq) {
|
||||||
window.open(this.paymentReq, '_blank', 'noopener')
|
window.open(this.paymentReq, '_blank', 'noopener')
|
||||||
|
|
|
||||||
|
|
@ -80,20 +80,18 @@
|
||||||
"
|
"
|
||||||
></q-select>
|
></q-select>
|
||||||
<div class="row q-col-gutter-md q-pt-lg items-center">
|
<div class="row q-col-gutter-md q-pt-lg items-center">
|
||||||
<div v-if="allowFiatCheckout" class="col-auto">
|
<div v-if="showPaymentMethodSelector" class="col-auto">
|
||||||
<q-option-group
|
<q-option-group
|
||||||
v-model="formDialog.data.payment_method"
|
v-model="formDialog.data.payment_method"
|
||||||
inline
|
inline
|
||||||
:options="[
|
:options="paymentMethodOptions"
|
||||||
{label: 'Lightning', value: 'lightning'},
|
|
||||||
{
|
|
||||||
label: fiatCheckoutLabel,
|
|
||||||
value: 'fiat'
|
|
||||||
}
|
|
||||||
]"
|
|
||||||
></q-option-group>
|
></q-option-group>
|
||||||
</div>
|
</div>
|
||||||
<div :class="allowFiatCheckout ? 'col-12 col-md-3' : 'col-12'">
|
<div
|
||||||
|
:class="
|
||||||
|
showPaymentMethodSelector ? 'col-12 col-md-3' : 'col-12'
|
||||||
|
"
|
||||||
|
>
|
||||||
<q-input
|
<q-input
|
||||||
filled
|
filled
|
||||||
dense
|
dense
|
||||||
|
|
@ -140,7 +138,7 @@
|
||||||
|
|
||||||
<q-dialog v-model="receive.show" position="top" @hide="closeReceiveDialog">
|
<q-dialog v-model="receive.show" position="top" @hide="closeReceiveDialog">
|
||||||
<q-card
|
<q-card
|
||||||
v-if="!receive.paymentReq"
|
v-if="!receive.paymentReq && !receive.isOnchain"
|
||||||
class="q-pa-lg q-pt-xl lnbits__dialog-card"
|
class="q-pa-lg q-pt-xl lnbits__dialog-card"
|
||||||
>
|
>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
@ -175,6 +173,51 @@
|
||||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
|
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
|
||||||
</div>
|
</div>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
<q-card
|
||||||
|
v-else-if="receive.isOnchain"
|
||||||
|
class="q-pa-lg q-pt-xl lnbits__dialog-card"
|
||||||
|
>
|
||||||
|
<div class="text-center q-mb-lg">
|
||||||
|
<div class="text-h6 q-mb-sm">Pay with Bitcoin</div>
|
||||||
|
<lnbits-qrcode
|
||||||
|
:href="onchainPaymentUri"
|
||||||
|
:value="onchainPaymentUri.toUpperCase()"
|
||||||
|
></lnbits-qrcode>
|
||||||
|
<div class="text-body1 q-mt-md">
|
||||||
|
<strong
|
||||||
|
>{{
|
||||||
|
(receive.onchainAmountSat / 100000000).toFixed(8)
|
||||||
|
}}
|
||||||
|
BTC</strong
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="text-caption text-grey q-mt-xs"
|
||||||
|
style="word-break: break-all"
|
||||||
|
>
|
||||||
|
{{ receive.onchainAddress }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row q-mt-lg q-col-gutter-sm">
|
||||||
|
<q-btn
|
||||||
|
outline
|
||||||
|
color="grey"
|
||||||
|
@click="utils.copyText(receive.onchainAddress)"
|
||||||
|
>Copy address</q-btn
|
||||||
|
>
|
||||||
|
<q-btn
|
||||||
|
v-if="mempoolAddressUrl"
|
||||||
|
outline
|
||||||
|
color="grey"
|
||||||
|
type="a"
|
||||||
|
:href="mempoolAddressUrl"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
>View on mempool</q-btn
|
||||||
|
>
|
||||||
|
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
|
||||||
|
</div>
|
||||||
|
</q-card>
|
||||||
<q-card v-else class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
<q-card v-else class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||||
<div class="text-center q-mb-lg">
|
<div class="text-center q-mb-lg">
|
||||||
<lnbits-qrcode
|
<lnbits-qrcode
|
||||||
|
|
|
||||||
|
|
@ -128,7 +128,8 @@ window.PageEvents = {
|
||||||
promo_codes: []
|
promo_codes: []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
onchainWallets: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
@ -136,6 +137,42 @@ 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) {
|
||||||
|
|
@ -403,6 +440,8 @@ 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',
|
||||||
|
|
@ -425,6 +464,12 @@ 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
|
||||||
|
|
@ -437,6 +482,8 @@ 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',
|
||||||
|
|
|
||||||
|
|
@ -265,6 +265,18 @@
|
||||||
>
|
>
|
||||||
<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">
|
||||||
|
|
@ -602,6 +614,42 @@
|
||||||
</div>
|
</div>
|
||||||
</q-expansion-item>
|
</q-expansion-item>
|
||||||
|
|
||||||
|
<q-expansion-item
|
||||||
|
group="advanced"
|
||||||
|
icon="currency_bitcoin"
|
||||||
|
label="Onchain payments"
|
||||||
|
>
|
||||||
|
<div class="q-mt-lg">
|
||||||
|
<div class="text-caption q-mb-md">
|
||||||
|
Accept Bitcoin onchain payments. Requires the Watchonly
|
||||||
|
extension.
|
||||||
|
</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>
|
||||||
|
</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"
|
||||||
|
|
|
||||||
166
views_api.py
166
views_api.py
|
|
@ -16,7 +16,7 @@ from fastapi import (
|
||||||
WebSocketDisconnect,
|
WebSocketDisconnect,
|
||||||
)
|
)
|
||||||
from fastapi.responses import StreamingResponse
|
from fastapi.responses import StreamingResponse
|
||||||
from lnbits.core.crud import get_user
|
from lnbits.core.crud import get_standalone_payment, get_user
|
||||||
from lnbits.core.crud.assets import get_public_asset
|
from lnbits.core.crud.assets import get_public_asset
|
||||||
from lnbits.core.crud.wallets import get_wallet
|
from lnbits.core.crud.wallets import get_wallet
|
||||||
from lnbits.core.models import WalletTypeInfo
|
from lnbits.core.models import WalletTypeInfo
|
||||||
|
|
@ -30,6 +30,7 @@ from lnbits.decorators import (
|
||||||
)
|
)
|
||||||
from lnbits.helpers import generate_filter_params_openapi
|
from lnbits.helpers import generate_filter_params_openapi
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
from lnbits.tasks import internal_invoice_queue_put
|
||||||
from lnbits.utils.exchange_rates import (
|
from lnbits.utils.exchange_rates import (
|
||||||
fiat_amount_as_satoshis,
|
fiat_amount_as_satoshis,
|
||||||
get_fiat_rate_satoshis,
|
get_fiat_rate_satoshis,
|
||||||
|
|
@ -65,7 +66,14 @@ from .models import (
|
||||||
ensure_ticket_waves,
|
ensure_ticket_waves,
|
||||||
get_active_ticket_waves,
|
get_active_ticket_waves,
|
||||||
)
|
)
|
||||||
from .services import refund_tickets, resend_ticket_email_notification
|
from .services import (
|
||||||
|
fetch_onchain_address,
|
||||||
|
fetch_watchonly_config,
|
||||||
|
fetch_watchonly_wallet,
|
||||||
|
fetch_watchonly_wallets,
|
||||||
|
refund_tickets,
|
||||||
|
resend_ticket_email_notification,
|
||||||
|
)
|
||||||
from .tasks import deregister_payment_listener, register_payment_listener
|
from .tasks import deregister_payment_listener, register_payment_listener
|
||||||
|
|
||||||
events_api_router = APIRouter(prefix="/api/v1/events")
|
events_api_router = APIRouter(prefix="/api/v1/events")
|
||||||
|
|
@ -74,6 +82,76 @@ qr_api_router = APIRouter(prefix="/api/v1")
|
||||||
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"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def _validate_watchonly_settings(
|
||||||
|
*,
|
||||||
|
wallet,
|
||||||
|
onchain_enabled: bool,
|
||||||
|
onchain_wallet_id: str | None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
if not onchain_enabled:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
|
detail="Onchain payments are not enabled for this event.",
|
||||||
|
)
|
||||||
|
if not onchain_wallet_id:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
|
detail="No watchonly wallet configured for onchain payments.",
|
||||||
|
)
|
||||||
|
status = await _get_watchonly_status(wallet)
|
||||||
|
if not status["available"]:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
|
detail=status["message"] or "Watchonly extension is not available.",
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
watch_wallet = await fetch_watchonly_wallet(wallet.inkey, onchain_wallet_id)
|
||||||
|
except Exception as exc:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
|
detail=f"Cannot access watchonly wallet: {exc!s}",
|
||||||
|
) from exc
|
||||||
|
if watch_wallet.get("network") != status["network"]:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
|
detail="Watchonly wallet network does not match user watchonly config.",
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"watch_wallet": watch_wallet,
|
||||||
|
"network": status["network"],
|
||||||
|
"mempool_endpoint": status["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"}
|
||||||
|
|
||||||
|
|
@ -120,6 +198,13 @@ async def api_events(
|
||||||
return await get_events(wallet_ids)
|
return await get_events(wallet_ids)
|
||||||
|
|
||||||
|
|
||||||
|
@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:
|
||||||
event = await get_event(event_id)
|
event = await get_event(event_id)
|
||||||
|
|
@ -371,7 +456,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"}:
|
if payment_method not in {"lightning", "fiat", "onchain"}:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
detail="Unsupported payment method.",
|
detail="Unsupported payment method.",
|
||||||
|
|
@ -438,6 +523,10 @@ 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_address = None
|
||||||
|
onchain_mempool_endpoint = 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):
|
||||||
invoice_unit = selected_wave.currency
|
invoice_unit = selected_wave.currency
|
||||||
|
|
@ -461,9 +550,42 @@ 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":
|
||||||
|
invoice_unit = "sat"
|
||||||
|
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.",
|
||||||
|
)
|
||||||
|
validation = await _validate_watchonly_settings(
|
||||||
|
wallet=wallet_record,
|
||||||
|
onchain_enabled=event.extra.onchain_enabled,
|
||||||
|
onchain_wallet_id=event.extra.onchain_wallet_id,
|
||||||
|
)
|
||||||
|
address_data = await fetch_onchain_address(
|
||||||
|
wallet_record.inkey, event.extra.onchain_wallet_id or ""
|
||||||
|
)
|
||||||
|
onchain_address = address_data.get("address")
|
||||||
|
onchain_mempool_endpoint = validation.get("mempool_endpoint")
|
||||||
else:
|
else:
|
||||||
invoice_unit = "sat"
|
invoice_unit = "sat"
|
||||||
|
|
||||||
|
if payment_method == "onchain":
|
||||||
|
payment = await create_payment_request(
|
||||||
|
wallet_id=event.wallet,
|
||||||
|
invoice_data=CreateInvoice(
|
||||||
|
out=False,
|
||||||
|
amount=float(onchain_amount_sat or 0),
|
||||||
|
unit="sat",
|
||||||
|
internal=True,
|
||||||
|
labels=["onchain"],
|
||||||
|
memo=f"{event_id}",
|
||||||
|
extra=extra,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else:
|
||||||
payment = await create_payment_request(
|
payment = await create_payment_request(
|
||||||
wallet_id=event.wallet,
|
wallet_id=event.wallet,
|
||||||
invoice_data=CreateInvoice(
|
invoice_data=CreateInvoice(
|
||||||
|
|
@ -488,7 +610,10 @@ async def api_ticket_create(
|
||||||
"refund_address": refund_address,
|
"refund_address": refund_address,
|
||||||
"nostr_identifier": nostr_identifier,
|
"nostr_identifier": nostr_identifier,
|
||||||
"ticket_base_url": str(request.base_url).rstrip("/"),
|
"ticket_base_url": str(request.base_url).rstrip("/"),
|
||||||
"sats_paid": payment.sat,
|
"sats_paid": (
|
||||||
|
onchain_amount_sat if payment_method == "onchain" else payment.sat
|
||||||
|
),
|
||||||
|
"onchain": payment_method == "onchain",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -498,6 +623,9 @@ async def api_ticket_create(
|
||||||
fiat_payment_request=getattr(payment, "extra", {}).get("fiat_payment_request"),
|
fiat_payment_request=getattr(payment, "extra", {}).get("fiat_payment_request"),
|
||||||
fiat_provider=getattr(payment, "fiat_provider", None) or fiat_provider,
|
fiat_provider=getattr(payment, "fiat_provider", None) or fiat_provider,
|
||||||
is_fiat=bool(getattr(payment, "fiat_provider", None) or fiat_provider),
|
is_fiat=bool(getattr(payment, "fiat_provider", None) or fiat_provider),
|
||||||
|
onchain_address=onchain_address,
|
||||||
|
onchain_mempool_endpoint=onchain_mempool_endpoint,
|
||||||
|
onchain_amount_sat=onchain_amount_sat,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -559,6 +687,36 @@ async def api_ticket_delete(
|
||||||
await delete_ticket(ticket_id)
|
await delete_ticket(ticket_id)
|
||||||
|
|
||||||
|
|
||||||
|
@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.",
|
||||||
|
)
|
||||||
|
payment = await get_standalone_payment(payment_hash)
|
||||||
|
if not payment:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.NOT_FOUND, detail="Payment does not exist."
|
||||||
|
)
|
||||||
|
await internal_invoice_queue_put(payment_hash)
|
||||||
|
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