Merge branch 'main' into FinalAdminUI
This commit is contained in:
commit
6cab77ece4
|
@ -338,36 +338,13 @@ async def delete_expired_invoices(
|
||||||
AND time < {db.timestamp_now} - {db.interval_seconds(2592000)}
|
AND time < {db.timestamp_now} - {db.interval_seconds(2592000)}
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
# then we delete all invoices whose expiry date is in the past
|
||||||
# then we delete all expired invoices, checking one by one
|
|
||||||
rows = await (conn or db).fetchall(
|
|
||||||
f"""
|
|
||||||
SELECT bolt11
|
|
||||||
FROM apipayments
|
|
||||||
WHERE pending = true
|
|
||||||
AND bolt11 IS NOT NULL
|
|
||||||
AND amount > 0 AND time < {db.timestamp_now} - {db.interval_seconds(86400)}
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
logger.debug(f"Checking expiry of {len(rows)} invoices")
|
|
||||||
for i, (payment_request,) in enumerate(rows):
|
|
||||||
try:
|
|
||||||
invoice = bolt11.decode(payment_request)
|
|
||||||
except:
|
|
||||||
continue
|
|
||||||
|
|
||||||
expiration_date = datetime.datetime.fromtimestamp(invoice.date + invoice.expiry)
|
|
||||||
if expiration_date > datetime.datetime.utcnow():
|
|
||||||
continue
|
|
||||||
logger.debug(
|
|
||||||
f"Deleting expired invoice {i}/{len(rows)}: {invoice.payment_hash} (expired: {expiration_date})"
|
|
||||||
)
|
|
||||||
await (conn or db).execute(
|
await (conn or db).execute(
|
||||||
"""
|
f"""
|
||||||
DELETE FROM apipayments
|
DELETE FROM apipayments
|
||||||
WHERE pending = true AND hash = ?
|
WHERE pending = true AND amount > 0
|
||||||
""",
|
AND expiry < {db.timestamp_now}
|
||||||
(invoice.payment_hash,),
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -395,12 +372,19 @@ async def create_payment(
|
||||||
# previous_payment = await get_wallet_payment(wallet_id, payment_hash, conn=conn)
|
# previous_payment = await get_wallet_payment(wallet_id, payment_hash, conn=conn)
|
||||||
# assert previous_payment is None, "Payment already exists"
|
# assert previous_payment is None, "Payment already exists"
|
||||||
|
|
||||||
|
try:
|
||||||
|
invoice = bolt11.decode(payment_request)
|
||||||
|
expiration_date = datetime.datetime.fromtimestamp(invoice.date + invoice.expiry)
|
||||||
|
except:
|
||||||
|
# assume maximum bolt11 expiry of 31 days to be on the safe side
|
||||||
|
expiration_date = datetime.datetime.now() + datetime.timedelta(days=31)
|
||||||
|
|
||||||
await (conn or db).execute(
|
await (conn or 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, webhook)
|
amount, pending, memo, fee, extra, webhook, expiry)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
wallet_id,
|
wallet_id,
|
||||||
|
@ -416,6 +400,7 @@ async def create_payment(
|
||||||
if extra and extra != {} and type(extra) is dict
|
if extra and extra != {} and type(extra) is dict
|
||||||
else None,
|
else None,
|
||||||
webhook,
|
webhook,
|
||||||
|
db.datetime_to_timestamp(expiration_date),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,10 @@
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
from sqlalchemy.exc import OperationalError # type: ignore
|
from sqlalchemy.exc import OperationalError # type: ignore
|
||||||
|
|
||||||
|
from lnbits import bolt11
|
||||||
|
|
||||||
|
|
||||||
async def m000_create_migrations_table(db):
|
async def m000_create_migrations_table(db):
|
||||||
await db.execute(
|
await db.execute(
|
||||||
|
@ -190,7 +195,71 @@ async def m005_balance_check_balance_notify(db):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def m006_create_admin_settings_table(db):
|
async def m006_add_invoice_expiry_to_apipayments(db):
|
||||||
|
"""
|
||||||
|
Adds invoice expiry column to apipayments.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
await db.execute("ALTER TABLE apipayments ADD COLUMN expiry TIMESTAMP")
|
||||||
|
except OperationalError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
async def m007_set_invoice_expiries(db):
|
||||||
|
"""
|
||||||
|
Precomputes invoice expiry for existing pending incoming payments.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
rows = await (
|
||||||
|
await db.execute(
|
||||||
|
f"""
|
||||||
|
SELECT bolt11, checking_id
|
||||||
|
FROM apipayments
|
||||||
|
WHERE pending = true
|
||||||
|
AND amount > 0
|
||||||
|
AND bolt11 IS NOT NULL
|
||||||
|
AND expiry IS NULL
|
||||||
|
AND time < {db.timestamp_now}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
).fetchall()
|
||||||
|
if len(rows):
|
||||||
|
logger.info(f"Mirgraion: Checking expiry of {len(rows)} invoices")
|
||||||
|
for i, (
|
||||||
|
payment_request,
|
||||||
|
checking_id,
|
||||||
|
) in enumerate(rows):
|
||||||
|
try:
|
||||||
|
invoice = bolt11.decode(payment_request)
|
||||||
|
if invoice.expiry is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
expiration_date = datetime.datetime.fromtimestamp(
|
||||||
|
invoice.date + invoice.expiry
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
f"Mirgraion: {i+1}/{len(rows)} setting expiry of invoice {invoice.payment_hash} to {expiration_date}"
|
||||||
|
)
|
||||||
|
await db.execute(
|
||||||
|
"""
|
||||||
|
UPDATE apipayments SET expiry = ?
|
||||||
|
WHERE checking_id = ? AND amount > 0
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
db.datetime_to_timestamp(expiration_date),
|
||||||
|
checking_id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
except OperationalError:
|
||||||
|
# this is necessary now because it may be the case that this migration will
|
||||||
|
# run twice in some environments.
|
||||||
|
# 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 m008_create_admin_settings_table(db):
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"""
|
"""
|
||||||
CREATE TABLE IF NOT EXISTS settings (
|
CREATE TABLE IF NOT EXISTS settings (
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
|
import datetime
|
||||||
import hashlib
|
import hashlib
|
||||||
import hmac
|
import hmac
|
||||||
import json
|
import json
|
||||||
|
import time
|
||||||
from sqlite3 import Row
|
from sqlite3 import Row
|
||||||
from typing import Dict, List, NamedTuple, Optional
|
from typing import Dict, List, NamedTuple, Optional
|
||||||
|
|
||||||
|
@ -85,6 +87,7 @@ class Payment(BaseModel):
|
||||||
bolt11: str
|
bolt11: str
|
||||||
preimage: str
|
preimage: str
|
||||||
payment_hash: str
|
payment_hash: str
|
||||||
|
expiry: Optional[float]
|
||||||
extra: Optional[Dict] = {}
|
extra: Optional[Dict] = {}
|
||||||
wallet_id: str
|
wallet_id: str
|
||||||
webhook: Optional[str]
|
webhook: Optional[str]
|
||||||
|
@ -103,6 +106,7 @@ class Payment(BaseModel):
|
||||||
fee=row["fee"],
|
fee=row["fee"],
|
||||||
memo=row["memo"],
|
memo=row["memo"],
|
||||||
time=row["time"],
|
time=row["time"],
|
||||||
|
expiry=row["expiry"],
|
||||||
wallet_id=row["wallet"],
|
wallet_id=row["wallet"],
|
||||||
webhook=row["webhook"],
|
webhook=row["webhook"],
|
||||||
webhook_status=row["webhook_status"],
|
webhook_status=row["webhook_status"],
|
||||||
|
@ -130,6 +134,10 @@ class Payment(BaseModel):
|
||||||
def is_out(self) -> bool:
|
def is_out(self) -> bool:
|
||||||
return self.amount < 0
|
return self.amount < 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_expired(self) -> bool:
|
||||||
|
return self.expiry < time.time() if self.expiry else False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_uncheckable(self) -> bool:
|
def is_uncheckable(self) -> bool:
|
||||||
return self.checking_id.startswith("internal_")
|
return self.checking_id.startswith("internal_")
|
||||||
|
@ -173,7 +181,13 @@ class Payment(BaseModel):
|
||||||
|
|
||||||
logger.debug(f"Status: {status}")
|
logger.debug(f"Status: {status}")
|
||||||
|
|
||||||
if self.is_out and status.failed:
|
if self.is_in and status.pending and self.is_expired and self.expiry:
|
||||||
|
expiration_date = datetime.datetime.fromtimestamp(self.expiry)
|
||||||
|
logger.debug(
|
||||||
|
f"Deleting expired incoming pending payment {self.checking_id}: expired {expiration_date}"
|
||||||
|
)
|
||||||
|
await self.delete(conn)
|
||||||
|
elif self.is_out and status.failed:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"Deleting outgoing failed payment {self.checking_id}: {status}"
|
f"Deleting outgoing failed payment {self.checking_id}: {status}"
|
||||||
)
|
)
|
||||||
|
|
18
lnbits/db.py
18
lnbits/db.py
|
@ -29,6 +29,13 @@ class Compat:
|
||||||
return f"{seconds}"
|
return f"{seconds}"
|
||||||
return "<nothing>"
|
return "<nothing>"
|
||||||
|
|
||||||
|
def datetime_to_timestamp(self, date: datetime.datetime):
|
||||||
|
if self.type in {POSTGRES, COCKROACH}:
|
||||||
|
return date.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
elif self.type == SQLITE:
|
||||||
|
return time.mktime(date.timetuple())
|
||||||
|
return "<nothing>"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def timestamp_now(self) -> str:
|
def timestamp_now(self) -> str:
|
||||||
if self.type in {POSTGRES, COCKROACH}:
|
if self.type in {POSTGRES, COCKROACH}:
|
||||||
|
@ -125,6 +132,8 @@ class Database(Compat):
|
||||||
import psycopg2 # type: ignore
|
import psycopg2 # type: ignore
|
||||||
|
|
||||||
def _parse_timestamp(value, _):
|
def _parse_timestamp(value, _):
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
f = "%Y-%m-%d %H:%M:%S.%f"
|
f = "%Y-%m-%d %H:%M:%S.%f"
|
||||||
if not "." in value:
|
if not "." in value:
|
||||||
f = "%Y-%m-%d %H:%M:%S"
|
f = "%Y-%m-%d %H:%M:%S"
|
||||||
|
@ -149,14 +158,7 @@ class Database(Compat):
|
||||||
|
|
||||||
psycopg2.extensions.register_type(
|
psycopg2.extensions.register_type(
|
||||||
psycopg2.extensions.new_type(
|
psycopg2.extensions.new_type(
|
||||||
(1184, 1114),
|
(1184, 1114), "TIMESTAMP2INT", _parse_timestamp
|
||||||
"TIMESTAMP2INT",
|
|
||||||
_parse_timestamp
|
|
||||||
# lambda value, curs: time.mktime(
|
|
||||||
# datetime.datetime.strptime(
|
|
||||||
# value, "%Y-%m-%d %H:%M:%S.%f"
|
|
||||||
# ).timetuple()
|
|
||||||
# ),
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{% extends "public.html" %} {% block toolbar_title %} {% raw %} {{name}} Cashu
|
{% extends "public.html" %} {% block toolbar_title %} {% raw %} Cashu {% endraw
|
||||||
{% endraw %} {% endblock %} {% block footer %}{% endblock %} {% block
|
%} - {{mint_name}} {% endblock %} {% block footer %}{% endblock %} {% block
|
||||||
page_container %}
|
page_container %}
|
||||||
<q-page-container>
|
<q-page-container>
|
||||||
<q-page>
|
<q-page>
|
||||||
|
@ -752,7 +752,13 @@ page_container %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row q-mt-lg">
|
<div class="row q-mt-lg">
|
||||||
<q-btn @click="redeem" color="primary">Receive Tokens</q-btn>
|
<q-btn @click="redeem" color="primary">Receive</q-btn>
|
||||||
|
<q-btn
|
||||||
|
unelevated
|
||||||
|
icon="content_copy"
|
||||||
|
class="q-mx-0"
|
||||||
|
@click="copyText(receiveData.tokensBase64)"
|
||||||
|
></q-btn>
|
||||||
<q-btn
|
<q-btn
|
||||||
unelevated
|
unelevated
|
||||||
icon="photo_camera"
|
icon="photo_camera"
|
||||||
|
|
|
@ -27,11 +27,17 @@ async def index(
|
||||||
|
|
||||||
@cashu_ext.get("/wallet")
|
@cashu_ext.get("/wallet")
|
||||||
async def wallet(request: Request, mint_id: str):
|
async def wallet(request: Request, mint_id: str):
|
||||||
|
cashu = await get_cashu(mint_id)
|
||||||
|
if not cashu:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
|
||||||
|
)
|
||||||
return cashu_renderer().TemplateResponse(
|
return cashu_renderer().TemplateResponse(
|
||||||
"cashu/wallet.html",
|
"cashu/wallet.html",
|
||||||
{
|
{
|
||||||
"request": request,
|
"request": request,
|
||||||
"web_manifest": f"/cashu/manifest/{mint_id}.webmanifest",
|
"web_manifest": f"/cashu/manifest/{mint_id}.webmanifest",
|
||||||
|
"mint_name": cashu.name,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -41,7 +47,7 @@ async def cashu(request: Request, mintID):
|
||||||
cashu = await get_cashu(mintID)
|
cashu = await get_cashu(mintID)
|
||||||
if not cashu:
|
if not cashu:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="TPoS does not exist."
|
status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
|
||||||
)
|
)
|
||||||
return cashu_renderer().TemplateResponse(
|
return cashu_renderer().TemplateResponse(
|
||||||
"cashu/mint.html",
|
"cashu/mint.html",
|
||||||
|
@ -54,7 +60,7 @@ async def manifest(cashu_id: str):
|
||||||
cashu = await get_cashu(cashu_id)
|
cashu = await get_cashu(cashu_id)
|
||||||
if not cashu:
|
if not cashu:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="TPoS does not exist."
|
status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -185,6 +185,7 @@ window.LNbits = {
|
||||||
bolt11: data.bolt11,
|
bolt11: data.bolt11,
|
||||||
preimage: data.preimage,
|
preimage: data.preimage,
|
||||||
payment_hash: data.payment_hash,
|
payment_hash: data.payment_hash,
|
||||||
|
expiry: data.expiry,
|
||||||
extra: data.extra,
|
extra: data.extra,
|
||||||
wallet_id: data.wallet_id,
|
wallet_id: data.wallet_id,
|
||||||
webhook: data.webhook,
|
webhook: data.webhook,
|
||||||
|
@ -196,6 +197,11 @@ window.LNbits = {
|
||||||
'YYYY-MM-DD HH:mm'
|
'YYYY-MM-DD HH:mm'
|
||||||
)
|
)
|
||||||
obj.dateFrom = moment(obj.date).fromNow()
|
obj.dateFrom = moment(obj.date).fromNow()
|
||||||
|
obj.expirydate = Quasar.utils.date.formatDate(
|
||||||
|
new Date(obj.expiry * 1000),
|
||||||
|
'YYYY-MM-DD HH:mm'
|
||||||
|
)
|
||||||
|
obj.expirydateFrom = moment(obj.expirydate).fromNow()
|
||||||
obj.msat = obj.amount
|
obj.msat = obj.amount
|
||||||
obj.sat = obj.msat / 1000
|
obj.sat = obj.msat / 1000
|
||||||
obj.tag = obj.extra.tag
|
obj.tag = obj.extra.tag
|
||||||
|
|
|
@ -192,9 +192,13 @@ Vue.component('lnbits-payment-details', {
|
||||||
</q-badge>
|
</q-badge>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-3"><b>Date</b>:</div>
|
<div class="col-3"><b>Created</b>:</div>
|
||||||
<div class="col-9">{{ payment.date }} ({{ payment.dateFrom }})</div>
|
<div class="col-9">{{ payment.date }} ({{ payment.dateFrom }})</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-3"><b>Expiry</b>:</div>
|
||||||
|
<div class="col-9">{{ payment.expirydate }} ({{ payment.expirydateFrom }})</div>
|
||||||
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-3"><b>Description</b>:</div>
|
<div class="col-3"><b>Description</b>:</div>
|
||||||
<div class="col-9">{{ payment.memo }}</div>
|
<div class="col-9">{{ payment.memo }}</div>
|
||||||
|
|
|
@ -147,7 +147,7 @@ async def check_pending_payments():
|
||||||
)
|
)
|
||||||
# we delete expired invoices once upon the first pending check
|
# we delete expired invoices once upon the first pending check
|
||||||
if incoming:
|
if incoming:
|
||||||
logger.info("Task: deleting all expired invoices")
|
logger.debug("Task: deleting all expired invoices")
|
||||||
start_time: float = time.time()
|
start_time: float = time.time()
|
||||||
await delete_expired_invoices(conn=conn)
|
await delete_expired_invoices(conn=conn)
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|
Binary file not shown.
Loading…
Reference in New Issue
Block a user