fix: use satspay for onchain payments (#56)
* use satspay * safeguard satspay * add fasttrack and 0conf
This commit is contained in:
parent
da16b209cd
commit
99b43ff851
7 changed files with 170 additions and 177 deletions
|
|
@ -47,6 +47,8 @@ class EventExtra(BaseModel):
|
||||||
notification_body: str = ""
|
notification_body: str = ""
|
||||||
onchain_enabled: bool = False
|
onchain_enabled: bool = False
|
||||||
onchain_wallet_id: str | None = None
|
onchain_wallet_id: str | None = None
|
||||||
|
onchain_zeroconf: bool = False
|
||||||
|
onchain_fasttrack: bool = False
|
||||||
|
|
||||||
|
|
||||||
class CreateEvent(BaseModel):
|
class CreateEvent(BaseModel):
|
||||||
|
|
@ -113,6 +115,8 @@ class TicketExtra(BaseModel):
|
||||||
nostr_notification_sent: bool = False
|
nostr_notification_sent: bool = False
|
||||||
refunded: bool = False
|
refunded: bool = False
|
||||||
onchain: bool = False
|
onchain: bool = False
|
||||||
|
onchain_address: str | None = None
|
||||||
|
satspay_charge_id: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class CreateTicket(BaseModel):
|
class CreateTicket(BaseModel):
|
||||||
|
|
@ -170,9 +174,8 @@ 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
|
onchain_amount_sat: int | None = None
|
||||||
|
satspay_charge_url: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class TicketFilters(FilterModel):
|
class TicketFilters(FilterModel):
|
||||||
|
|
|
||||||
15
services.py
15
services.py
|
|
@ -62,16 +62,27 @@ async def fetch_watchonly_wallet(api_key: str, wallet_id: str) -> dict[str, Any]
|
||||||
return resp.json()
|
return resp.json()
|
||||||
|
|
||||||
|
|
||||||
async def fetch_onchain_address(api_key: str, wallet_id: str) -> dict[str, Any]:
|
async def get_satspay_charge(api_key: str, charge_id: str) -> dict[str, Any]:
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
resp = await client.get(
|
resp = await client.get(
|
||||||
url=f"http://{settings.host}:{settings.port}/watchonly/api/v1/address/{wallet_id}",
|
url=f"http://{settings.host}:{settings.port}/satspay/api/v1/charge/{charge_id}",
|
||||||
headers={"X-API-KEY": api_key},
|
headers={"X-API-KEY": api_key},
|
||||||
)
|
)
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
return resp.json()
|
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()
|
||||||
|
|
||||||
|
|
||||||
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,11 +27,7 @@ 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
|
||||||
|
|
@ -99,16 +95,6 @@ window.PageEventsDisplay = {
|
||||||
options.push({label: 'Bitcoin', value: 'onchain'})
|
options.push({label: 'Bitcoin', value: 'onchain'})
|
||||||
}
|
}
|
||||||
return options
|
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: {
|
||||||
|
|
@ -161,11 +147,7 @@ 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) {
|
||||||
|
|
@ -203,11 +185,7 @@ 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,
|
||||||
|
|
@ -235,13 +213,15 @@ window.PageEventsDisplay = {
|
||||||
: 'lightning'
|
: 'lightning'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
const isOnchain = Boolean(data.onchain_address)
|
if (data.satspay_charge_url) {
|
||||||
const isFiat = !isOnchain && Boolean(data.is_fiat)
|
window.location.href = data.satspay_charge_url
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const isFiat = Boolean(data.is_fiat)
|
||||||
this.paymentReq = isFiat
|
this.paymentReq = isFiat
|
||||||
? data.fiat_payment_request || null
|
? data.fiat_payment_request || null
|
||||||
: isOnchain
|
: data.payment_request
|
||||||
? null
|
|
||||||
: data.payment_request
|
|
||||||
this.paymentHash = data.payment_hash
|
this.paymentHash = data.payment_hash
|
||||||
|
|
||||||
this.paymentDismissMsg = Quasar.Notify.create({
|
this.paymentDismissMsg = Quasar.Notify.create({
|
||||||
|
|
@ -252,11 +232,7 @@ 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')
|
||||||
|
|
@ -291,8 +267,12 @@ window.PageEventsDisplay = {
|
||||||
console.error('WebSocket error:', error)
|
console.error('WebSocket error:', error)
|
||||||
}
|
}
|
||||||
ws.onclose = () => {
|
ws.onclose = () => {
|
||||||
if (this.paymentWebsocket === ws) {
|
if (this.paymentWebsocket !== ws) return
|
||||||
this.paymentWebsocket = null
|
this.paymentWebsocket = null
|
||||||
|
if (this.receive.show) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.receive.show) this.paymentWatcher(paymentHash)
|
||||||
|
}, 3000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -138,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 && !receive.isOnchain"
|
v-if="!receive.paymentReq"
|
||||||
class="q-pa-lg q-pt-xl lnbits__dialog-card"
|
class="q-pa-lg q-pt-xl lnbits__dialog-card"
|
||||||
>
|
>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
@ -173,51 +173,6 @@
|
||||||
<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
|
||||||
|
|
|
||||||
|
|
@ -442,6 +442,8 @@ window.PageEvents = {
|
||||||
nostr_notifications: false,
|
nostr_notifications: false,
|
||||||
onchain_enabled: false,
|
onchain_enabled: false,
|
||||||
onchain_wallet_id: null,
|
onchain_wallet_id: null,
|
||||||
|
onchain_zeroconf: false,
|
||||||
|
onchain_fasttrack: false,
|
||||||
ticket_waves: [
|
ticket_waves: [
|
||||||
{
|
{
|
||||||
id: 'primary',
|
id: 'primary',
|
||||||
|
|
|
||||||
|
|
@ -616,13 +616,14 @@
|
||||||
|
|
||||||
<q-expansion-item
|
<q-expansion-item
|
||||||
group="advanced"
|
group="advanced"
|
||||||
icon="currency_bitcoin"
|
icon="view_in_ar"
|
||||||
label="Onchain payments"
|
label="Onchain payments"
|
||||||
>
|
>
|
||||||
<div class="q-mt-lg">
|
<div class="q-mt-lg">
|
||||||
<div class="text-caption q-mb-md">
|
<div class="text-caption q-mb-md">
|
||||||
Accept Bitcoin onchain payments. Requires the Watchonly
|
Accept Bitcoin onchain payments. Requires the
|
||||||
extension.
|
<strong>SatsPay</strong> and <strong>Watchonly</strong>
|
||||||
|
extensions to be installed and enabled.
|
||||||
</div>
|
</div>
|
||||||
<q-toggle
|
<q-toggle
|
||||||
v-model="formDialog.data.extra.onchain_enabled"
|
v-model="formDialog.data.extra.onchain_enabled"
|
||||||
|
|
@ -647,6 +648,18 @@
|
||||||
map-options
|
map-options
|
||||||
hint="Bitcoin watchonly wallet for receiving onchain payments"
|
hint="Bitcoin watchonly wallet for receiving onchain payments"
|
||||||
></q-select>
|
></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>
|
</div>
|
||||||
</q-expansion-item>
|
</q-expansion-item>
|
||||||
|
|
||||||
|
|
|
||||||
203
views_api.py
203
views_api.py
|
|
@ -28,13 +28,14 @@ from lnbits.decorators import (
|
||||||
require_admin_key,
|
require_admin_key,
|
||||||
require_invoice_key,
|
require_invoice_key,
|
||||||
)
|
)
|
||||||
from lnbits.helpers import generate_filter_params_openapi
|
from lnbits.helpers import generate_filter_params_openapi, urlsafe_short_hash
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
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,
|
||||||
satoshis_amount_as_fiat,
|
satoshis_amount_as_fiat,
|
||||||
)
|
)
|
||||||
|
from loguru import logger
|
||||||
from PIL import Image, ImageDraw
|
from PIL import Image, ImageDraw
|
||||||
|
|
||||||
from .crud import (
|
from .crud import (
|
||||||
|
|
@ -66,10 +67,10 @@ from .models import (
|
||||||
get_active_ticket_waves,
|
get_active_ticket_waves,
|
||||||
)
|
)
|
||||||
from .services import (
|
from .services import (
|
||||||
fetch_onchain_address,
|
create_satspay_charge,
|
||||||
fetch_watchonly_config,
|
fetch_watchonly_config,
|
||||||
fetch_watchonly_wallet,
|
|
||||||
fetch_watchonly_wallets,
|
fetch_watchonly_wallets,
|
||||||
|
get_satspay_charge,
|
||||||
refund_tickets,
|
refund_tickets,
|
||||||
resend_ticket_email_notification,
|
resend_ticket_email_notification,
|
||||||
send_ticket_notification_in_background,
|
send_ticket_notification_in_background,
|
||||||
|
|
@ -116,47 +117,6 @@ async def _get_watchonly_status(wallet) -> dict[str, Any]:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
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"}
|
||||||
|
|
||||||
|
|
@ -528,8 +488,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_address = None
|
|
||||||
onchain_mempool_endpoint = None
|
|
||||||
onchain_amount_sat = None
|
onchain_amount_sat = None
|
||||||
|
|
||||||
if payment_method == "fiat":
|
if payment_method == "fiat":
|
||||||
|
|
@ -556,7 +514,6 @@ async def api_ticket_create(
|
||||||
detail="No fiat payment provider configured for this event.",
|
detail="No fiat payment provider configured for this event.",
|
||||||
)
|
)
|
||||||
elif payment_method == "onchain":
|
elif payment_method == "onchain":
|
||||||
invoice_unit = "sat"
|
|
||||||
onchain_amount_sat = int(price)
|
onchain_amount_sat = int(price)
|
||||||
wallet_record = await get_wallet(event.wallet)
|
wallet_record = await get_wallet(event.wallet)
|
||||||
if not wallet_record:
|
if not wallet_record:
|
||||||
|
|
@ -564,44 +521,82 @@ async def api_ticket_create(
|
||||||
status_code=HTTPStatus.NOT_FOUND,
|
status_code=HTTPStatus.NOT_FOUND,
|
||||||
detail="Event wallet does not exist.",
|
detail="Event wallet does not exist.",
|
||||||
)
|
)
|
||||||
validation = await _validate_watchonly_settings(
|
if not event.extra.onchain_enabled:
|
||||||
wallet=wallet_record,
|
raise HTTPException(
|
||||||
onchain_enabled=event.extra.onchain_enabled,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
onchain_wallet_id=event.extra.onchain_wallet_id,
|
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("/")
|
||||||
|
webhook_url = f"{base_url}/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"],
|
||||||
|
},
|
||||||
)
|
)
|
||||||
address_data = await fetch_onchain_address(
|
|
||||||
wallet_record.inkey, event.extra.onchain_wallet_id or ""
|
return TicketPaymentRequest(
|
||||||
|
payment_hash=ticket_id,
|
||||||
|
onchain_amount_sat=onchain_amount_sat,
|
||||||
|
satspay_charge_url=f"/satspay/{charge['id']}",
|
||||||
)
|
)
|
||||||
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(
|
||||||
payment = await create_payment_request(
|
wallet_id=event.wallet,
|
||||||
wallet_id=event.wallet,
|
invoice_data=CreateInvoice(
|
||||||
invoice_data=CreateInvoice(
|
out=False,
|
||||||
out=False,
|
amount=fiat_amount if payment_method == "fiat" else price,
|
||||||
amount=float(onchain_amount_sat or 0),
|
unit=invoice_unit,
|
||||||
unit="sat",
|
fiat_provider=fiat_provider,
|
||||||
internal=True,
|
memo=f"{event_id}",
|
||||||
labels=["onchain"],
|
extra=extra,
|
||||||
memo=f"{event_id}",
|
),
|
||||||
extra=extra,
|
)
|
||||||
),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
payment = await create_payment_request(
|
|
||||||
wallet_id=event.wallet,
|
|
||||||
invoice_data=CreateInvoice(
|
|
||||||
out=False,
|
|
||||||
amount=fiat_amount if payment_method == "fiat" else price,
|
|
||||||
unit=invoice_unit,
|
|
||||||
fiat_provider=fiat_provider,
|
|
||||||
memo=f"{event_id}",
|
|
||||||
extra=extra,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
await create_ticket(
|
await create_ticket(
|
||||||
payment_hash=payment.payment_hash,
|
payment_hash=payment.payment_hash,
|
||||||
wallet=event.wallet,
|
wallet=event.wallet,
|
||||||
|
|
@ -615,10 +610,8 @@ 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": (
|
"sats_paid": payment.sat,
|
||||||
onchain_amount_sat if payment_method == "onchain" else payment.sat
|
"onchain": False,
|
||||||
),
|
|
||||||
"onchain": payment_method == "onchain",
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -628,9 +621,6 @@ 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,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -692,6 +682,45 @@ 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, request: Request) -> 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")
|
@tickets_api_router.put("/{payment_hash}/onchain-confirm")
|
||||||
async def api_ticket_onchain_confirm(
|
async def api_ticket_onchain_confirm(
|
||||||
payment_hash: str,
|
payment_hash: str,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue