Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down
43 changes: 43 additions & 0 deletions services.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
59 changes: 52 additions & 7 deletions static/js/display.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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,
Expand All @@ -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({
Expand All @@ -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')
Expand Down
63 changes: 53 additions & 10 deletions static/js/display.vue
Original file line number Diff line number Diff line change
Expand Up @@ -80,20 +80,18 @@
"
></q-select>
<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
v-model="formDialog.data.payment_method"
inline
:options="[
{label: 'Lightning', value: 'lightning'},
{
label: fiatCheckoutLabel,
value: 'fiat'
}
]"
:options="paymentMethodOptions"
></q-option-group>
</div>
<div :class="allowFiatCheckout ? 'col-12 col-md-3' : 'col-12'">
<div
:class="
showPaymentMethodSelector ? 'col-12 col-md-3' : 'col-12'
"
>
<q-input
filled
dense
Expand Down Expand Up @@ -140,7 +138,7 @@

<q-dialog v-model="receive.show" position="top" @hide="closeReceiveDialog">
<q-card
v-if="!receive.paymentReq"
v-if="!receive.paymentReq && !receive.isOnchain"
class="q-pa-lg q-pt-xl lnbits__dialog-card"
>
</q-card>
Expand Down Expand Up @@ -175,6 +173,51 @@
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
</div>
</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">
<div class="text-center q-mb-lg">
<lnbits-qrcode
Expand Down
49 changes: 48 additions & 1 deletion static/js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,14 +128,51 @@ window.PageEvents = {
promo_codes: []
}
}
}
},
onchainWallets: []
}
},
methods: {
shortenId(value) {
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) {
Expand Down Expand Up @@ -403,6 +440,8 @@ window.PageEvents = {
min_tickets: 1,
email_notifications: false,
nostr_notifications: false,
onchain_enabled: false,
onchain_wallet_id: null,
ticket_waves: [
{
id: 'primary',
Expand All @@ -425,6 +464,12 @@ window.PageEvents = {
}
}
this.formDialog.show = true
if (
this.formDialog.data.wallet &&
this.formDialog.data.extra?.onchain_enabled
) {
this.loadOnchainWallets()
}
},
resetEventDialog() {
this.formDialog.show = false
Expand All @@ -437,6 +482,8 @@ window.PageEvents = {
min_tickets: 1,
email_notifications: false,
nostr_notifications: false,
onchain_enabled: false,
onchain_wallet_id: null,
ticket_waves: [
{
id: 'primary',
Expand Down
Loading
Loading