Skip to content
Closed
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: 5 additions & 1 deletion crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,11 @@ async def get_tickets_paginated(
wallet_where.append(f":{key}")
values[key] = wallet_id

where = [f"wallet IN ({', '.join(wallet_where)})", "paid = true"]
where = [
f"wallet IN ({', '.join(wallet_where)})",
"(paid = true OR extra LIKE :onchain_pattern)",
]
values["onchain_pattern"] = '%"onchain": true%'

return await db.fetch_page(
"SELECT * FROM events.ticket",
Expand Down
2 changes: 2 additions & 0 deletions models.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ class TicketExtra(BaseModel):
nostr_notification_sent: bool = False
refunded: bool = False
onchain: bool = False
onchain_address: str | None = None
onchain_mempool_endpoint: str | None = None


class CreateTicket(BaseModel):
Expand Down
25 changes: 25 additions & 0 deletions services.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,31 @@ async def fetch_onchain_address(api_key: str, wallet_id: str) -> dict[str, Any]:
return resp.json()


async def check_onchain_payment(ticket: Ticket) -> bool:
address = ticket.extra.onchain_address
endpoint = ticket.extra.onchain_mempool_endpoint
expected_sats = ticket.extra.sats_paid or 0
if not address or not endpoint:
return False
async with httpx.AsyncClient() as client:
resp = await client.get(
f"{endpoint}/api/address/{address}/txs",
timeout=10.0,
)
resp.raise_for_status()
txs = resp.json()
for tx in txs:
if not tx.get("status", {}).get("confirmed"):
continue
for vout in tx.get("vout", []):
if (
vout.get("scriptpubkey_address") == address
and vout.get("value", 0) >= expected_sats
):
return True
return False


async def set_ticket_paid(ticket: Ticket) -> Ticket:
if ticket.paid:
return ticket
Expand Down
34 changes: 31 additions & 3 deletions static/js/display.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ window.PageEventsDisplay = {
mempoolEndpoint: null
},
paymentDismissMsg: null,
paymentWebsocket: null
paymentWebsocket: null,
onchainPollTimer: null
}
},
async created() {
Expand Down Expand Up @@ -148,6 +149,10 @@ window.PageEventsDisplay = {
},

closeReceiveDialog() {
if (this.onchainPollTimer) {
clearTimeout(this.onchainPollTimer)
this.onchainPollTimer = null
}
if (this.paymentDismissMsg) {
this.paymentDismissMsg()
this.paymentDismissMsg = null
Expand Down Expand Up @@ -262,10 +267,29 @@ window.PageEventsDisplay = {
window.open(this.paymentReq, '_blank', 'noopener')
}
this.paymentWatcher(this.paymentHash)
if (isOnchain) this.startOnchainPolling(this.paymentHash)
} catch (error) {
LNbits.utils.notifyApiError(error)
}
},
startOnchainPolling(paymentHash) {
if (this.onchainPollTimer) clearTimeout(this.onchainPollTimer)
const poll = async () => {
if (!this.receive.show) return
try {
await LNbits.api.request(
'POST',
`/events/api/v1/tickets/${paymentHash}/onchain-check`,
null
)
// confirmed — WebSocket fires paymentSuccess, don't reschedule
} catch (error) {
if (!this.receive.show) return
this.onchainPollTimer = setTimeout(poll, 30000)
}
}
this.onchainPollTimer = setTimeout(poll, 30000)
},
paymentWatcher(paymentHash) {
if (this.paymentWebsocket) {
this.paymentWebsocket.close()
Expand All @@ -291,8 +315,12 @@ window.PageEventsDisplay = {
console.error('WebSocket error:', error)
}
ws.onclose = () => {
if (this.paymentWebsocket === ws) {
this.paymentWebsocket = null
if (this.paymentWebsocket !== ws) return
this.paymentWebsocket = null
if (this.receive.show) {
setTimeout(() => {
if (this.receive.show) this.paymentWatcher(paymentHash)
}, 3000)
}
}
}
Expand Down
30 changes: 30 additions & 0 deletions views_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
get_active_ticket_waves,
)
from .services import (
check_onchain_payment,
fetch_onchain_address,
fetch_watchonly_config,
fetch_watchonly_wallet,
Expand Down Expand Up @@ -619,6 +620,8 @@ async def api_ticket_create(
onchain_amount_sat if payment_method == "onchain" else payment.sat
),
"onchain": payment_method == "onchain",
"onchain_address": onchain_address,
"onchain_mempool_endpoint": onchain_mempool_endpoint,
},
)

Expand Down Expand Up @@ -692,6 +695,33 @@ async def api_ticket_delete(
await delete_ticket(ticket_id)


@tickets_api_router.post("/{payment_hash}/onchain-check")
async def api_ticket_onchain_check(payment_hash: str) -> Ticket:
ticket = await get_ticket(payment_hash)
if not ticket:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Ticket does not exist."
)
if ticket.paid:
return ticket
if not ticket.extra.onchain:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail="Not an onchain ticket.",
)
found = await check_onchain_payment(ticket)
if not found:
raise HTTPException(
status_code=HTTPStatus.PAYMENT_REQUIRED,
detail="Payment not found on chain.",
)
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.put("/{payment_hash}/onchain-confirm")
async def api_ticket_onchain_confirm(
payment_hash: str,
Expand Down
Loading