mirror of
https://github.com/lnbits/lnbits-legend.git
synced 2025-01-19 05:33:47 +01:00
finish webhooks for normal invoices with two extra columns.
This commit is contained in:
parent
4623220316
commit
1c922a5ddc
@ -253,13 +253,14 @@ async def create_payment(
|
|||||||
preimage: Optional[str] = None,
|
preimage: Optional[str] = None,
|
||||||
pending: bool = True,
|
pending: bool = True,
|
||||||
extra: Optional[Dict] = None,
|
extra: Optional[Dict] = None,
|
||||||
|
webhook: Optional[str] = None,
|
||||||
) -> Payment:
|
) -> Payment:
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO apipayments
|
INSERT INTO apipayments
|
||||||
(wallet, checking_id, bolt11, hash, preimage,
|
(wallet, checking_id, bolt11, hash, preimage,
|
||||||
amount, pending, memo, fee, extra)
|
amount, pending, memo, fee, extra, webhook)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
wallet_id,
|
wallet_id,
|
||||||
@ -272,6 +273,7 @@ async def create_payment(
|
|||||||
memo,
|
memo,
|
||||||
fee,
|
fee,
|
||||||
json.dumps(extra) if extra and extra != {} and type(extra) is dict else None,
|
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:
|
async def update_payment_status(checking_id: str, pending: bool) -> None:
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"UPDATE apipayments SET pending = ? WHERE checking_id = ?",
|
"UPDATE apipayments SET pending = ? WHERE checking_id = ?", (int(pending), checking_id,),
|
||||||
(
|
|
||||||
int(pending),
|
|
||||||
checking_id,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
# catching errors like this won't be necessary in anymore now that we
|
||||||
# keep track of db versions so no migration ever runs twice.
|
# keep track of db versions so no migration ever runs twice.
|
||||||
pass
|
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")
|
||||||
|
@ -40,11 +40,7 @@ class Wallet(NamedTuple):
|
|||||||
hashing_key = hashlib.sha256(self.id.encode("utf-8")).digest()
|
hashing_key = hashlib.sha256(self.id.encode("utf-8")).digest()
|
||||||
linking_key = hmac.digest(hashing_key, domain.encode("utf-8"), "sha256")
|
linking_key = hmac.digest(hashing_key, domain.encode("utf-8"), "sha256")
|
||||||
|
|
||||||
return SigningKey.from_string(
|
return SigningKey.from_string(linking_key, curve=SECP256k1, hashfunc=hashlib.sha256,)
|
||||||
linking_key,
|
|
||||||
curve=SECP256k1,
|
|
||||||
hashfunc=hashlib.sha256,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def get_payment(self, payment_hash: str) -> Optional["Payment"]:
|
async def get_payment(self, payment_hash: str) -> Optional["Payment"]:
|
||||||
from .crud import get_wallet_payment
|
from .crud import get_wallet_payment
|
||||||
@ -84,6 +80,8 @@ class Payment(NamedTuple):
|
|||||||
payment_hash: str
|
payment_hash: str
|
||||||
extra: Dict
|
extra: Dict
|
||||||
wallet_id: str
|
wallet_id: str
|
||||||
|
webhook: str
|
||||||
|
webhook_status: int
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_row(cls, row: Row):
|
def from_row(cls, row: Row):
|
||||||
@ -99,6 +97,8 @@ class Payment(NamedTuple):
|
|||||||
memo=row["memo"],
|
memo=row["memo"],
|
||||||
time=row["time"],
|
time=row["time"],
|
||||||
wallet_id=row["wallet"],
|
wallet_id=row["wallet"],
|
||||||
|
webhook=row["webhook"],
|
||||||
|
webhook_status=row["webhook_status"],
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -28,6 +28,7 @@ async def create_invoice(
|
|||||||
memo: str,
|
memo: str,
|
||||||
description_hash: Optional[bytes] = None,
|
description_hash: Optional[bytes] = None,
|
||||||
extra: Optional[Dict] = None,
|
extra: Optional[Dict] = None,
|
||||||
|
webhook: Optional[str] = None,
|
||||||
) -> Tuple[str, str]:
|
) -> Tuple[str, str]:
|
||||||
await db.begin()
|
await db.begin()
|
||||||
invoice_memo = None if description_hash else memo
|
invoice_memo = None if description_hash else memo
|
||||||
@ -50,6 +51,7 @@ async def create_invoice(
|
|||||||
amount=amount_msat,
|
amount=amount_msat,
|
||||||
memo=storeable_memo,
|
memo=storeable_memo,
|
||||||
extra=extra,
|
extra=extra,
|
||||||
|
webhook=webhook,
|
||||||
)
|
)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
@ -131,10 +133,7 @@ async def pay_invoice(
|
|||||||
payment: PaymentResponse = WALLET.pay_invoice(payment_request)
|
payment: PaymentResponse = WALLET.pay_invoice(payment_request)
|
||||||
if payment.ok and payment.checking_id:
|
if payment.ok and payment.checking_id:
|
||||||
await create_payment(
|
await create_payment(
|
||||||
checking_id=payment.checking_id,
|
checking_id=payment.checking_id, fee=payment.fee_msat, preimage=payment.preimage, **payment_kwargs,
|
||||||
fee=payment.fee_msat,
|
|
||||||
preimage=payment.preimage,
|
|
||||||
**payment_kwargs,
|
|
||||||
)
|
)
|
||||||
await delete_payment(temp_id)
|
await delete_payment(temp_id)
|
||||||
else:
|
else:
|
||||||
@ -154,8 +153,7 @@ async def redeem_lnurl_withdraw(wallet_id: str, res: LnurlWithdrawResponse, memo
|
|||||||
|
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
await client.get(
|
await client.get(
|
||||||
res.callback.base,
|
res.callback.base, params={**res.callback.query_params, **{"k1": res.k1, "pr": payment_request}},
|
||||||
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:
|
async with httpx.AsyncClient() as client:
|
||||||
r = await client.get(
|
r = await client.get(
|
||||||
callback,
|
callback,
|
||||||
params={
|
params={"k1": k1.hex(), "key": key.verifying_key.to_string("compressed").hex(), "sig": sig.hex(),},
|
||||||
"k1": k1.hex(),
|
|
||||||
"key": key.verifying_key.to_string("compressed").hex(),
|
|
||||||
"sig": sig.hex(),
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
resp = json.loads(r.text)
|
resp = json.loads(r.text)
|
||||||
@ -225,9 +219,7 @@ async def perform_lnurlauth(callback: str) -> Optional[LnurlErrorResponse]:
|
|||||||
|
|
||||||
return LnurlErrorResponse(reason=resp["reason"])
|
return LnurlErrorResponse(reason=resp["reason"])
|
||||||
except (KeyError, json.decoder.JSONDecodeError):
|
except (KeyError, json.decoder.JSONDecodeError):
|
||||||
return LnurlErrorResponse(
|
return LnurlErrorResponse(reason=r.text[:200] + "..." if len(r.text) > 200 else r.text,)
|
||||||
reason=r.text[:200] + "..." if len(r.text) > 200 else r.text,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def check_invoice_status(wallet_id: str, payment_hash: str) -> PaymentStatus:
|
async def check_invoice_status(wallet_id: str, payment_hash: str) -> PaymentStatus:
|
||||||
|
@ -3,6 +3,8 @@ import httpx
|
|||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from lnbits.tasks import register_invoice_listener
|
from lnbits.tasks import register_invoice_listener
|
||||||
|
from . import db
|
||||||
|
from .models import Payment
|
||||||
|
|
||||||
sse_listeners: List[trio.MemorySendChannel] = []
|
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 def wait_for_paid_invoices(invoice_paid_chan: trio.MemoryReceiveChannel):
|
||||||
async for payment in invoice_paid_chan:
|
async for payment in invoice_paid_chan:
|
||||||
# send information to sse channel
|
# send information to sse channel
|
||||||
for send_channel in sse_listeners:
|
await dispatch_sse(payment)
|
||||||
try:
|
|
||||||
send_channel.send_nowait(payment)
|
|
||||||
except trio.WouldBlock:
|
|
||||||
print("removing sse listener", send_channel)
|
|
||||||
sse_listeners.remove(send_channel)
|
|
||||||
|
|
||||||
# dispatch webhook
|
# dispatch webhook
|
||||||
if payment.extra and "webhook" in payment.extra:
|
if payment.webhook and not payment.webhook_status:
|
||||||
async with httpx.AsyncClient() as client:
|
await dispatch_webhook(payment)
|
||||||
try:
|
|
||||||
await client.post(payment.extra["webhook"], json=payment._asdict(), timeout=40)
|
|
||||||
except (httpx.ConnectError, httpx.RequestError):
|
async def dispatch_sse(payment: Payment):
|
||||||
pass
|
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),
|
||||||
|
)
|
||||||
|
@ -55,8 +55,9 @@
|
|||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
<code
|
<code
|
||||||
>curl -X POST {{ request.url_root }}api/v1/payments -d '{"out": false,
|
>curl -X POST {{ request.url_root }}api/v1/payments -d '{"out": false,
|
||||||
"amount": <int>, "memo": <string>}' -H "X-Api-Key:
|
"amount": <int>, "memo": <string>, "webhook":
|
||||||
<i>{{ wallet.inkey }}</i>" -H "Content-type: application/json"</code
|
<url:string>}' -H "X-Api-Key: <i>{{ wallet.inkey }}</i>" -H
|
||||||
|
"Content-type: application/json"</code
|
||||||
>
|
>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
@ -46,6 +46,7 @@ async def api_payments():
|
|||||||
"description_hash": {"type": "string", "empty": False, "required": True, "excludes": "memo"},
|
"description_hash": {"type": "string", "empty": False, "required": True, "excludes": "memo"},
|
||||||
"lnurl_callback": {"type": "string", "nullable": True, "required": False},
|
"lnurl_callback": {"type": "string", "nullable": True, "required": False},
|
||||||
"extra": {"type": "dict", "nullable": True, "required": False},
|
"extra": {"type": "dict", "nullable": True, "required": False},
|
||||||
|
"webhook": {"type": "string", "empty": False, "required": False},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
async def api_payments_create_invoice():
|
async def api_payments_create_invoice():
|
||||||
@ -62,7 +63,8 @@ async def api_payments_create_invoice():
|
|||||||
amount=g.data["amount"],
|
amount=g.data["amount"],
|
||||||
memo=memo,
|
memo=memo,
|
||||||
description_hash=description_hash,
|
description_hash=description_hash,
|
||||||
extra=g.data["extra"],
|
extra=g.data.get("extra"),
|
||||||
|
webhook=g.data.get("webhook"),
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
await db.rollback()
|
await db.rollback()
|
||||||
|
@ -140,7 +140,10 @@ window.LNbits = {
|
|||||||
'bolt11',
|
'bolt11',
|
||||||
'preimage',
|
'preimage',
|
||||||
'payment_hash',
|
'payment_hash',
|
||||||
'extra'
|
'extra',
|
||||||
|
'wallet_id',
|
||||||
|
'webhook',
|
||||||
|
'webhook_status'
|
||||||
],
|
],
|
||||||
data
|
data
|
||||||
)
|
)
|
||||||
|
@ -204,6 +204,15 @@ Vue.component('lnbits-payment-details', {
|
|||||||
<div class="col-3"><b>Payment hash</b>:</div>
|
<div class="col-3"><b>Payment hash</b>:</div>
|
||||||
<div class="col-9 text-wrap mono">{{ payment.payment_hash }}</div>
|
<div class="col-9 text-wrap mono">{{ payment.payment_hash }}</div>
|
||||||
</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="row" v-if="hasPreimage">
|
||||||
<div class="col-3"><b>Payment proof</b>:</div>
|
<div class="col-3"><b>Payment proof</b>:</div>
|
||||||
<div class="col-9 text-wrap mono">{{ payment.preimage }}</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
|
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() {
|
hasTag() {
|
||||||
return this.payment.extra && !!this.payment.extra.tag
|
return this.payment.extra && !!this.payment.extra.tag
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user