Browse Source

finish webhooks for normal invoices with two extra columns.

master
fiatjaf 4 years ago
committed by fiatjaf
parent
commit
1c922a5ddc
  1. 12
      lnbits/core/crud.py
  2. 10
      lnbits/core/migrations.py
  3. 10
      lnbits/core/models.py
  4. 20
      lnbits/core/services.py
  5. 46
      lnbits/core/tasks.py
  6. 5
      lnbits/core/templates/core/_api_docs.html
  7. 4
      lnbits/core/views/api.py
  8. 5
      lnbits/static/js/base.js
  9. 22
      lnbits/static/js/components.js

12
lnbits/core/crud.py

@ -253,13 +253,14 @@ async def create_payment(
preimage: Optional[str] = None,
pending: bool = True,
extra: Optional[Dict] = None,
webhook: Optional[str] = None,
) -> Payment:
await db.execute(
"""
INSERT INTO apipayments
(wallet, checking_id, bolt11, hash, preimage,
amount, pending, memo, fee, extra)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
amount, pending, memo, fee, extra, webhook)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
wallet_id,
@ -272,6 +273,7 @@ async def create_payment(
memo,
fee,
json.dumps(extra) if extra and extra != {} and type(extra) is dict else None,
webhook,
),
)
@ -283,11 +285,7 @@ async def create_payment(
async def update_payment_status(checking_id: str, pending: bool) -> None:
await db.execute(
"UPDATE apipayments SET pending = ? WHERE checking_id = ?",
(
int(pending),
checking_id,
),
"UPDATE apipayments SET pending = ? WHERE checking_id = ?", (int(pending), checking_id,),
)

10
lnbits/core/migrations.py

@ -120,3 +120,13 @@ async def m002_add_fields_to_apipayments(db):
# catching errors like this won't be necessary in anymore now that we
# keep track of db versions so no migration ever runs twice.
pass
async def m003_add_invoice_webhook(db):
"""
Special column for webhook endpoints that can be assigned
to each different invoice.
"""
await db.execute("ALTER TABLE apipayments ADD COLUMN webhook TEXT")
await db.execute("ALTER TABLE apipayments ADD COLUMN webhook_status TEXT")

10
lnbits/core/models.py

@ -40,11 +40,7 @@ class Wallet(NamedTuple):
hashing_key = hashlib.sha256(self.id.encode("utf-8")).digest()
linking_key = hmac.digest(hashing_key, domain.encode("utf-8"), "sha256")
return SigningKey.from_string(
linking_key,
curve=SECP256k1,
hashfunc=hashlib.sha256,
)
return SigningKey.from_string(linking_key, curve=SECP256k1, hashfunc=hashlib.sha256,)
async def get_payment(self, payment_hash: str) -> Optional["Payment"]:
from .crud import get_wallet_payment
@ -84,6 +80,8 @@ class Payment(NamedTuple):
payment_hash: str
extra: Dict
wallet_id: str
webhook: str
webhook_status: int
@classmethod
def from_row(cls, row: Row):
@ -99,6 +97,8 @@ class Payment(NamedTuple):
memo=row["memo"],
time=row["time"],
wallet_id=row["wallet"],
webhook=row["webhook"],
webhook_status=row["webhook_status"],
)
@property

20
lnbits/core/services.py

@ -28,6 +28,7 @@ async def create_invoice(
memo: str,
description_hash: Optional[bytes] = None,
extra: Optional[Dict] = None,
webhook: Optional[str] = None,
) -> Tuple[str, str]:
await db.begin()
invoice_memo = None if description_hash else memo
@ -50,6 +51,7 @@ async def create_invoice(
amount=amount_msat,
memo=storeable_memo,
extra=extra,
webhook=webhook,
)
await db.commit()
@ -131,10 +133,7 @@ async def pay_invoice(
payment: PaymentResponse = WALLET.pay_invoice(payment_request)
if payment.ok and payment.checking_id:
await create_payment(
checking_id=payment.checking_id,
fee=payment.fee_msat,
preimage=payment.preimage,
**payment_kwargs,
checking_id=payment.checking_id, fee=payment.fee_msat, preimage=payment.preimage, **payment_kwargs,
)
await delete_payment(temp_id)
else:
@ -154,8 +153,7 @@ async def redeem_lnurl_withdraw(wallet_id: str, res: LnurlWithdrawResponse, memo
async with httpx.AsyncClient() as client:
await client.get(
res.callback.base,
params={**res.callback.query_params, **{"k1": res.k1, "pr": payment_request}},
res.callback.base, params={**res.callback.query_params, **{"k1": res.k1, "pr": payment_request}},
)
@ -212,11 +210,7 @@ async def perform_lnurlauth(callback: str) -> Optional[LnurlErrorResponse]:
async with httpx.AsyncClient() as client:
r = await client.get(
callback,
params={
"k1": k1.hex(),
"key": key.verifying_key.to_string("compressed").hex(),
"sig": sig.hex(),
},
params={"k1": k1.hex(), "key": key.verifying_key.to_string("compressed").hex(), "sig": sig.hex(),},
)
try:
resp = json.loads(r.text)
@ -225,9 +219,7 @@ async def perform_lnurlauth(callback: str) -> Optional[LnurlErrorResponse]:
return LnurlErrorResponse(reason=resp["reason"])
except (KeyError, json.decoder.JSONDecodeError):
return LnurlErrorResponse(
reason=r.text[:200] + "..." if len(r.text) > 200 else r.text,
)
return LnurlErrorResponse(reason=r.text[:200] + "..." if len(r.text) > 200 else r.text,)
async def check_invoice_status(wallet_id: str, payment_hash: str) -> PaymentStatus:

46
lnbits/core/tasks.py

@ -3,6 +3,8 @@ import httpx
from typing import List
from lnbits.tasks import register_invoice_listener
from . import db
from .models import Payment
sse_listeners: List[trio.MemorySendChannel] = []
@ -16,17 +18,37 @@ async def register_listeners():
async def wait_for_paid_invoices(invoice_paid_chan: trio.MemoryReceiveChannel):
async for payment in invoice_paid_chan:
# send information to sse channel
for send_channel in sse_listeners:
try:
send_channel.send_nowait(payment)
except trio.WouldBlock:
print("removing sse listener", send_channel)
sse_listeners.remove(send_channel)
await dispatch_sse(payment)
# dispatch webhook
if payment.extra and "webhook" in payment.extra:
async with httpx.AsyncClient() as client:
try:
await client.post(payment.extra["webhook"], json=payment._asdict(), timeout=40)
except (httpx.ConnectError, httpx.RequestError):
pass
if payment.webhook and not payment.webhook_status:
await dispatch_webhook(payment)
async def dispatch_sse(payment: Payment):
for send_channel in sse_listeners:
try:
send_channel.send_nowait(payment)
except trio.WouldBlock:
print("removing sse listener", send_channel)
sse_listeners.remove(send_channel)
async def dispatch_webhook(payment: Payment):
async with httpx.AsyncClient() as client:
data = payment._asdict()
try:
r = await client.post(payment.webhook, json=data, timeout=40,)
await mark_webhook_sent(payment, r.status_code)
except (httpx.ConnectError, httpx.RequestError):
await mark_webhook_sent(payment, -1)
async def mark_webhook_sent(payment: Payment, status: int) -> None:
await db.execute(
"""
UPDATE apipayments SET webhook_status = ?
WHERE hash = ?
""",
(status, payment.payment_hash),
)

5
lnbits/core/templates/core/_api_docs.html

@ -55,8 +55,9 @@
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X POST {{ request.url_root }}api/v1/payments -d '{"out": false,
"amount": &lt;int&gt;, "memo": &lt;string&gt;}' -H "X-Api-Key:
<i>{{ wallet.inkey }}</i>" -H "Content-type: application/json"</code
"amount": &lt;int&gt;, "memo": &lt;string&gt;, "webhook":
&lt;url:string&gt;}' -H "X-Api-Key: <i>{{ wallet.inkey }}</i>" -H
"Content-type: application/json"</code
>
</q-card-section>
</q-card>

4
lnbits/core/views/api.py

@ -46,6 +46,7 @@ async def api_payments():
"description_hash": {"type": "string", "empty": False, "required": True, "excludes": "memo"},
"lnurl_callback": {"type": "string", "nullable": True, "required": False},
"extra": {"type": "dict", "nullable": True, "required": False},
"webhook": {"type": "string", "empty": False, "required": False},
}
)
async def api_payments_create_invoice():
@ -62,7 +63,8 @@ async def api_payments_create_invoice():
amount=g.data["amount"],
memo=memo,
description_hash=description_hash,
extra=g.data["extra"],
extra=g.data.get("extra"),
webhook=g.data.get("webhook"),
)
except Exception as exc:
await db.rollback()

5
lnbits/static/js/base.js

@ -140,7 +140,10 @@ window.LNbits = {
'bolt11',
'preimage',
'payment_hash',
'extra'
'extra',
'wallet_id',
'webhook',
'webhook_status'
],
data
)

22
lnbits/static/js/components.js

@ -204,6 +204,15 @@ Vue.component('lnbits-payment-details', {
<div class="col-3"><b>Payment hash</b>:</div>
<div class="col-9 text-wrap mono">{{ payment.payment_hash }}</div>
</div>
<div class="row" v-if="payment.webhook">
<div class="col-3"><b>Webhook</b>:</div>
<div class="col-9 text-wrap mono">
{{ payment.webhook }}
<q-badge :color="webhookStatusColor" text-color="white">
{{ webhookStatusText }}
</q-badge>
</div>
</div>
<div class="row" v-if="hasPreimage">
<div class="col-3"><b>Payment proof</b>:</div>
<div class="col-9 text-wrap mono">{{ payment.preimage }}</div>
@ -243,6 +252,19 @@ Vue.component('lnbits-payment-details', {
this.payment.extra.success_action
)
},
webhookStatusColor() {
return this.payment.webhook_status >= 300 ||
this.payment.webhook_status < 0
? 'red-10'
: !this.payment.webhook_status
? 'cyan-7'
: 'green-10'
},
webhookStatusText() {
return this.payment.webhook_status
? this.payment.webhook_status
: 'not sent yet'
},
hasTag() {
return this.payment.extra && !!this.payment.extra.tag
},

Loading…
Cancel
Save