Repository navigation
Expand file tree
/
Copy pathtasks.py
More file actions
222 lines (195 loc) · 7.73 KB
/
Copy pathtasks.py
File metadata and controls
222 lines (195 loc) · 7.73 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
import asyncio
import hashlib
import json
from urllib.parse import urlsplit
import httpx
import websockets
from bolt11 import decode as bolt11_decode
from lnbits.core.crud import get_payment, update_payment
from lnbits.core.models import Payment
from lnbits.task_manager import task_manager
from lnbits.tasks import register_invoice_listener
from loguru import logger
from pynostr.event import Event
from .crud import claim_pay_link_invoice, get_or_create_lnurlp_settings, get_pay_link
from .helpers import resolve_callback_addresses, validate_zap_request
from .models import PayLink, PayLinkInvoice
async def wait_for_paid_invoices():
invoice_queue = asyncio.Queue()
register_invoice_listener(invoice_queue, "ext_lnurlp")
while True:
payment = await invoice_queue.get()
try:
await on_invoice_paid(payment)
except Exception as exc:
logger.warning("LNURLp payment notification failed: {}", type(exc).__name__)
async def on_invoice_paid(payment: Payment):
if (
not payment.extra
or payment.extra.get("tag") != "lnurlp"
or not payment.success
or not payment.is_in
):
return
if payment.extra.get("wh_status"):
# this webhook has already been sent
return
pay_link_id = payment.extra.get("link")
if not pay_link_id:
logger.error("Invoice paid. But no pay link id found.")
return
pay_link = await get_pay_link(pay_link_id)
if not pay_link:
logger.error(f"Invoice paid. But Pay link `{pay_link_id}` not found.")
return
# shortcut: no retries after a crash; add durable delivery when required.
if not await claim_pay_link_invoice(
PayLinkInvoice(
payment_hash=payment.payment_hash,
pay_link=pay_link.id,
wallet_id=payment.wallet_id,
amount=payment.amount,
)
):
logger.debug(
"LNURLp invoice claim skipped: payment_hash={}, pay_link={}",
payment.payment_hash,
pay_link.id,
)
return
zap_receipt = None
if pay_link.zaps:
try:
zap_receipt = await send_zap(payment)
except Exception as exc:
logger.warning("LNURLp zap receipt failed: {}", type(exc).__name__)
await send_webhook(
payment, pay_link, zap_receipt.to_message() if zap_receipt else None
)
async def send_webhook(payment: Payment, pay_link: PayLink, zap_receipt=None):
if not pay_link.webhook_url:
return
try:
url = httpx.URL(pay_link.webhook_url)
addresses = await resolve_callback_addresses(str(url))
headers = (
json.loads(pay_link.webhook_headers) if pay_link.webhook_headers else {}
)
headers = httpx.Headers(headers)
headers["Host"] = urlsplit(str(url)).netloc
async with httpx.AsyncClient(follow_redirects=False, trust_env=False) as client:
for address in addresses:
try:
async with client.stream(
"POST",
url.copy_with(host=address),
json={
"payment_hash": payment.payment_hash,
"payment_request": payment.bolt11,
"amount": payment.amount,
"comment": (
payment.extra.get("comment") if payment.extra else None
),
"webhook_data": (
payment.extra.get("webhook_data")
if payment.extra
else None
),
"lnurlp": pay_link.id,
"body": (
json.loads(pay_link.webhook_body)
if pay_link.webhook_body
else ""
),
"zap_receipt": zap_receipt or "",
},
headers=headers,
extensions={"sni_hostname": url.host},
timeout=6,
) as response:
await mark_webhook_sent(
payment.checking_id,
response.status_code,
response.is_success,
(
"Webhook delivered."
if response.is_success
else "Webhook failed."
),
)
return
except (httpx.ConnectError, httpx.ConnectTimeout):
if address == addresses[-1]:
raise
except Exception as exc:
logger.warning(exc)
await mark_webhook_sent(
payment.checking_id, -1, False, "Webhook request failed."
)
async def mark_webhook_sent(
checking_id: str, status: int, is_success: bool, reason_phrase=""
) -> None:
payment = await get_payment(checking_id)
extra = payment.extra or {}
extra["wh_status"] = status # keep for backwards compability
extra["wh_success"] = is_success
extra["wh_message"] = reason_phrase
# Webhook responses can echo credentials into invoice-key-readable metadata.
extra["wh_response"] = ""
payment.extra = extra
await update_payment(payment)
# NIP-57 - load the zap request
async def send_zap(payment: Payment):
nostr = payment.extra.get("nostr") if payment.extra else None
if not nostr:
return
settings = await get_or_create_lnurlp_settings()
event = validate_zap_request(nostr, payment.amount, settings.public_key)
assert event.pubkey
invoice = bolt11_decode(payment.bolt11)
if (
invoice.payment_hash != payment.payment_hash
or invoice.amount_msat != payment.amount
or invoice.description_hash != hashlib.sha256(nostr.encode()).hexdigest()
):
raise ValueError("Zap request does not match the paid invoice.")
tags = [[tag[0], tag[1]] for tag in event.tags if tag[0] in {"p", "e", "a"}]
tags.append(["P", event.pubkey])
tags.append(["bolt11", payment.bolt11])
tags.append(["description", nostr])
zap_receipt = Event(
kind=9735,
tags=tags,
content="",
created_at=int(payment.updated_at.timestamp()),
)
zap_receipt.sign(settings.private_key.hex())
relays = next(tag[1:] for tag in event.tags if tag[0] == "relays")[:50]
for relay in relays:
task_manager.create_task(_send_to_relay(relay, zap_receipt.to_message()))
return zap_receipt
async def _send_to_relay(relay_url: str, event_message: str) -> None:
try:
addresses = await resolve_callback_addresses(relay_url)
parsed = urlsplit(relay_url)
for address in addresses:
connected = False
try:
async with websockets.connect(
relay_url,
host=address,
port=parsed.port or (443 if parsed.scheme == "wss" else 80),
proxy=None,
open_timeout=5,
close_timeout=1,
max_size=16384,
) as websocket:
connected = True
await websocket.send(event_message)
await asyncio.wait_for(websocket.recv(), timeout=5)
return
except (OSError, asyncio.TimeoutError):
if connected or address == addresses[-1]:
raise
except Exception as exc:
logger.warning("LNURLp relay delivery failed: {}", type(exc).__name__)