Merge remote-tracking branch 'origin/main' into gerty

This commit is contained in:
ben 2022-12-15 14:50:48 +00:00
commit ee79bdcfc8
21 changed files with 294 additions and 134 deletions

View file

@ -8,7 +8,7 @@ import warnings
from http import HTTPStatus from http import HTTPStatus
from fastapi import FastAPI, Request from fastapi import FastAPI, Request
from fastapi.exceptions import RequestValidationError from fastapi.exceptions import HTTPException, RequestValidationError
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.gzip import GZipMiddleware from fastapi.middleware.gzip import GZipMiddleware
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
@ -68,28 +68,6 @@ def create_app(config_object="lnbits.settings") -> FastAPI:
g().config = lnbits.settings g().config = lnbits.settings
g().base_url = f"http://{lnbits.settings.HOST}:{lnbits.settings.PORT}" g().base_url = f"http://{lnbits.settings.HOST}:{lnbits.settings.PORT}"
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(
request: Request, exc: RequestValidationError
):
# Only the browser sends "text/html" request
# not fail proof, but everything else get's a JSON response
if (
request.headers
and "accept" in request.headers
and "text/html" in request.headers["accept"]
):
return template_renderer().TemplateResponse(
"error.html",
{"request": request, "err": f"{exc.errors()} is not a valid UUID."},
)
return JSONResponse(
status_code=HTTPStatus.NO_CONTENT,
content={"detail": exc.errors()},
)
app.add_middleware(GZipMiddleware, minimum_size=1000) app.add_middleware(GZipMiddleware, minimum_size=1000)
check_funding_source(app) check_funding_source(app)
@ -192,12 +170,33 @@ def register_async_tasks(app):
def register_exception_handlers(app: FastAPI): def register_exception_handlers(app: FastAPI):
@app.exception_handler(Exception) @app.exception_handler(Exception)
async def basic_error(request: Request, err): async def exception_handler(request: Request, exc: Exception):
logger.error("handled error", traceback.format_exc())
logger.error("ERROR:", err)
etype, _, tb = sys.exc_info() etype, _, tb = sys.exc_info()
traceback.print_exception(etype, err, tb) traceback.print_exception(etype, exc, tb)
exc = traceback.format_exc() logger.error(f"Exception: {str(exc)}")
# Only the browser sends "text/html" request
# not fail proof, but everything else get's a JSON response
if (
request.headers
and "accept" in request.headers
and "text/html" in request.headers["accept"]
):
return template_renderer().TemplateResponse(
"error.html", {"request": request, "err": f"Error: {str(exc)}"}
)
return JSONResponse(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
content={"detail": str(exc)},
)
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(
request: Request, exc: RequestValidationError
):
logger.error(f"RequestValidationError: {str(exc)}")
# Only the browser sends "text/html" request
# not fail proof, but everything else get's a JSON response
if ( if (
request.headers request.headers
@ -205,12 +204,37 @@ def register_exception_handlers(app: FastAPI):
and "text/html" in request.headers["accept"] and "text/html" in request.headers["accept"]
): ):
return template_renderer().TemplateResponse( return template_renderer().TemplateResponse(
"error.html", {"request": request, "err": err} "error.html",
{"request": request, "err": f"Error: {str(exc)}"},
) )
return JSONResponse( return JSONResponse(
status_code=HTTPStatus.NO_CONTENT, status_code=HTTPStatus.BAD_REQUEST,
content={"detail": err}, content={"detail": str(exc)},
)
@app.exception_handler(HTTPException)
async def http_exception_handler(request: Request, exc: HTTPException):
logger.error(f"HTTPException {exc.status_code}: {exc.detail}")
# Only the browser sends "text/html" request
# not fail proof, but everything else get's a JSON response
if (
request.headers
and "accept" in request.headers
and "text/html" in request.headers["accept"]
):
return template_renderer().TemplateResponse(
"error.html",
{
"request": request,
"err": f"HTTP Error {exc.status_code}: {exc.detail}",
},
)
return JSONResponse(
status_code=exc.status_code,
content={"detail": exc.detail},
) )

View file

@ -339,37 +339,14 @@ 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 await (conn or db).execute(
rows = await (conn or db).fetchall(
f""" f"""
SELECT bolt11 DELETE FROM apipayments
FROM apipayments WHERE pending = true AND amount > 0
WHERE pending = true AND expiry < {db.timestamp_now}
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(
"""
DELETE FROM apipayments
WHERE pending = true AND hash = ?
""",
(invoice.payment_hash,),
)
# payments # payments
@ -396,12 +373,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,
@ -417,6 +401,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),
), ),
) )

View file

@ -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(
@ -188,3 +193,68 @@ async def m005_balance_check_balance_notify(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

View file

@ -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
@ -83,6 +85,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]
@ -101,6 +104,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"],
@ -128,6 +132,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_")
@ -170,7 +178,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}"
) )

View file

@ -702,9 +702,9 @@ async def api_auditor(wallet: WalletTypeInfo = Depends(get_key_type)):
node_balance, delta = None, None node_balance, delta = None, None
return { return {
"node_balance_msats": node_balance, "node_balance_msats": int(node_balance),
"lnbits_balance_msats": total_balance, "lnbits_balance_msats": int(total_balance),
"delta_msats": delta, "delta_msats": int(delta),
"timestamp": int(time.time()), "timestamp": int(time.time()),
} }

View file

@ -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:

View file

@ -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"

View file

@ -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 {

View file

@ -221,7 +221,7 @@ async def mint_coins(
status: PaymentStatus = await check_transaction_status(cashu.wallet, payment_hash) status: PaymentStatus = await check_transaction_status(cashu.wallet, payment_hash)
if status.paid != True: if LIGHTNING and status.paid != True:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.PAYMENT_REQUIRED, detail="Invoice not paid." status_code=HTTPStatus.PAYMENT_REQUIRED, detail="Invoice not paid."
) )
@ -265,37 +265,51 @@ async def melt_coins(
detail="Error: Tokens are from another mint.", detail="Error: Tokens are from another mint.",
) )
assert all([ledger._verify_proof(p) for p in proofs]), HTTPException( # set proofs as pending
status_code=HTTPStatus.BAD_REQUEST, await ledger._set_proofs_pending(proofs)
detail="Could not verify proofs.",
)
total_provided = sum([p["amount"] for p in proofs]) try:
invoice_obj = bolt11.decode(invoice) ledger._verify_proofs(proofs)
amount = math.ceil(invoice_obj.amount_msat / 1000)
internal_checking_id = await check_internal(invoice_obj.payment_hash) total_provided = sum([p["amount"] for p in proofs])
invoice_obj = bolt11.decode(invoice)
amount = math.ceil(invoice_obj.amount_msat / 1000)
if not internal_checking_id: internal_checking_id = await check_internal(invoice_obj.payment_hash)
fees_msat = fee_reserve(invoice_obj.amount_msat)
else:
fees_msat = 0
assert total_provided >= amount + fees_msat / 1000, Exception(
f"Provided proofs ({total_provided} sats) not enough for Lightning payment ({amount + fees_msat} sats)."
)
await pay_invoice( if not internal_checking_id:
wallet_id=cashu.wallet, fees_msat = fee_reserve(invoice_obj.amount_msat)
payment_request=invoice, else:
description=f"pay cashu invoice", fees_msat = 0
extra={"tag": "cashu", "cahsu_name": cashu.name}, assert total_provided >= amount + math.ceil(fees_msat / 1000), Exception(
) f"Provided proofs ({total_provided} sats) not enough for Lightning payment ({amount + fees_msat} sats)."
)
logger.debug(f"Cashu: Initiating payment of {total_provided} sats")
await pay_invoice(
wallet_id=cashu.wallet,
payment_request=invoice,
description=f"Pay cashu invoice",
extra={"tag": "cashu", "cashu_name": cashu.name},
)
logger.debug(
f"Cashu: Wallet {cashu.wallet} checking PaymentStatus of {invoice_obj.payment_hash}"
)
status: PaymentStatus = await check_transaction_status(
cashu.wallet, invoice_obj.payment_hash
)
if status.paid == True:
logger.debug("Cashu: Payment successful, invalidating proofs")
await ledger._invalidate_proofs(proofs)
except Exception as e:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=f"Cashu: {str(e)}",
)
finally:
# delete proofs from pending list
await ledger._unset_proofs_pending(proofs)
status: PaymentStatus = await check_transaction_status(
cashu.wallet, invoice_obj.payment_hash
)
if status.paid == True:
await ledger._invalidate_proofs(proofs)
return GetMeltResponse(paid=status.paid, preimage=status.preimage) return GetMeltResponse(paid=status.paid, preimage=status.preimage)
@ -333,7 +347,7 @@ async def check_fees(
fees_msat = fee_reserve(invoice_obj.amount_msat) fees_msat = fee_reserve(invoice_obj.amount_msat)
else: else:
fees_msat = 0 fees_msat = 0
return CheckFeesResponse(fee=fees_msat / 1000) return CheckFeesResponse(fee=math.ceil(fees_msat / 1000))
@cashu_ext.post("/api/v1/{cashu_id}/split") @cashu_ext.post("/api/v1/{cashu_id}/split")

View file

@ -37,7 +37,11 @@ async def call_webhook(charge: Charges):
json=public_charge(charge), json=public_charge(charge),
timeout=40, timeout=40,
) )
return {"webhook_success": r.is_success, "webhook_message": r.reason_phrase} return {
"webhook_success": r.is_success,
"webhook_message": r.reason_phrase,
"webhook_response": r.text,
}
except Exception as e: except Exception as e:
logger.warning(f"Failed to call webhook for charge {charge.id}") logger.warning(f"Failed to call webhook for charge {charge.id}")
logger.warning(e) logger.warning(e)

View file

@ -23,6 +23,7 @@ const mapCharge = (obj, oldObj = {}) => {
charge.displayUrl = ['/satspay/', obj.id].join('') charge.displayUrl = ['/satspay/', obj.id].join('')
charge.expanded = oldObj.expanded || false charge.expanded = oldObj.expanded || false
charge.pendingBalance = oldObj.pendingBalance || 0 charge.pendingBalance = oldObj.pendingBalance || 0
charge.extra = charge.extra ? JSON.parse(charge.extra) : charge.extra
return charge return charge
} }

View file

@ -227,7 +227,12 @@
> >
</div> </div>
<div class="col-4 q-pr-lg"> <div class="col-4 q-pr-lg">
<q-badge v-if="props.row.webhook_message" color="blue"> <q-badge
v-if="props.row.webhook_message"
@click="showWebhookResponseDialog(props.row.extra.webhook_response)"
color="blue"
class="cursor-pointer"
>
{{props.row.webhook_message }} {{props.row.webhook_message }}
</q-badge> </q-badge>
</div> </div>
@ -528,6 +533,23 @@
</q-form> </q-form>
</q-card> </q-card>
</q-dialog> </q-dialog>
<q-dialog v-model="showWebhookResponse" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-input
filled
dense
readonly
v-model.trim="webhookResponse"
type="textarea"
label="Response"
></q-input>
<div class="row q-mt-lg">
<q-btn flat v-close-popup color="grey" class="q-ml-auto">Close</q-btn>
</div>
</q-card>
</q-dialog>
</div> </div>
{% endblock %} {% block scripts %} {{ window_vars(user) }} {% endblock %} {% block scripts %} {{ window_vars(user) }}
<!-- lnbits/static/vendor <!-- lnbits/static/vendor
@ -669,7 +691,9 @@
data: { data: {
custom_css: '' custom_css: ''
} }
} },
showWebhookResponse: false,
webhookResponse: ''
} }
}, },
methods: { methods: {
@ -757,7 +781,6 @@
'/satspay/api/v1/themes', '/satspay/api/v1/themes',
this.g.user.wallets[0].inkey this.g.user.wallets[0].inkey
) )
console.log(data)
this.themeLinks = data.map(c => this.themeLinks = data.map(c =>
mapCSS( mapCSS(
c, c,
@ -852,14 +875,12 @@
}, },
updateformDialog: function (themeId) { updateformDialog: function (themeId) {
const theme = _.findWhere(this.themeLinks, {css_id: themeId}) const theme = _.findWhere(this.themeLinks, {css_id: themeId})
console.log(theme.css_id)
this.formDialogThemes.data.css_id = theme.css_id this.formDialogThemes.data.css_id = theme.css_id
this.formDialogThemes.data.title = theme.title this.formDialogThemes.data.title = theme.title
this.formDialogThemes.data.custom_css = theme.custom_css this.formDialogThemes.data.custom_css = theme.custom_css
this.formDialogThemes.show = true this.formDialogThemes.show = true
}, },
createTheme: async function (wallet, data) { createTheme: async function (wallet, data) {
console.log(data.css_id)
try { try {
if (data.css_id) { if (data.css_id) {
const resp = await LNbits.api.request( const resp = await LNbits.api.request(
@ -887,7 +908,6 @@
custom_css: '' custom_css: ''
} }
} catch (error) { } catch (error) {
console.log('cun')
LNbits.utils.notifyApiError(error) LNbits.utils.notifyApiError(error)
} }
}, },
@ -955,6 +975,10 @@
} }
}) })
}, },
showWebhookResponseDialog(webhookResponse) {
this.webhookResponse = webhookResponse
this.showWebhookResponse = true
},
exportchargeCSV: function () { exportchargeCSV: function () {
LNbits.utils.exportCSV( LNbits.utils.exportCSV(
this.chargesTable.columns, this.chargesTable.columns,

View file

@ -184,6 +184,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,
@ -195,6 +196,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

View file

@ -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>

View file

@ -145,7 +145,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(

8
poetry.lock generated
View file

@ -123,7 +123,7 @@ uvloop = ["uvloop (>=0.15.2)"]
[[package]] [[package]]
name = "cashu" name = "cashu"
version = "0.5.5" version = "0.6.0"
description = "Ecash wallet and mint with Bitcoin Lightning support" description = "Ecash wallet and mint with Bitcoin Lightning support"
category = "main" category = "main"
optional = false optional = false
@ -1144,7 +1144,7 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools"
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.10 | ^3.9 | ^3.8 | ^3.7" python-versions = "^3.10 | ^3.9 | ^3.8 | ^3.7"
content-hash = "53a18d7695f02e9ad24dc7d0863b5ae815c18f2f390ef20d7166a54b202642ff" content-hash = "7f75ca0b067a11f19520dc2121f0789e16738b573a8da84ba3838ed8a466a6e1"
[metadata.files] [metadata.files]
aiofiles = [ aiofiles = [
@ -1208,8 +1208,8 @@ black = [
{file = "black-22.10.0.tar.gz", hash = "sha256:f513588da599943e0cde4e32cc9879e825d58720d6557062d1098c5ad80080e1"}, {file = "black-22.10.0.tar.gz", hash = "sha256:f513588da599943e0cde4e32cc9879e825d58720d6557062d1098c5ad80080e1"},
] ]
cashu = [ cashu = [
{file = "cashu-0.5.5-py3-none-any.whl", hash = "sha256:c1d707479b852e503acca5ed53aa341b1880cd6bd70369488ec002d647970c9b"}, {file = "cashu-0.6.0-py3-none-any.whl", hash = "sha256:54096af145643aab45943b235f95a3357b0ec697835c1411e66523049ffb81f6"},
{file = "cashu-0.5.5.tar.gz", hash = "sha256:cc0349d3b6d9a2428cb575fee6280b20074ca9c20a1e2e9c68729a73c01f5f9d"}, {file = "cashu-0.6.0.tar.gz", hash = "sha256:503a90c4ca8d25d0b2c3f78a11b163c32902a726ea5b58e5337dc00eca8e96ad"},
] ]
Cerberus = [ Cerberus = [
{file = "Cerberus-1.3.4.tar.gz", hash = "sha256:d1b21b3954b2498d9a79edf16b3170a3ac1021df88d197dc2ce5928ba519237c"}, {file = "Cerberus-1.3.4.tar.gz", hash = "sha256:d1b21b3954b2498d9a79edf16b3170a3ac1021df88d197dc2ce5928ba519237c"},

View file

@ -64,7 +64,7 @@ protobuf = "^4.21.6"
Cerberus = "^1.3.4" Cerberus = "^1.3.4"
async-timeout = "^4.0.2" async-timeout = "^4.0.2"
pyln-client = "0.11.1" pyln-client = "0.11.1"
cashu = "0.5.5" cashu = "^0.6.0"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]

View file

@ -7,7 +7,7 @@ attrs==22.1.0 ; python_version >= "3.7" and python_version < "4.0"
base58==2.1.1 ; python_version >= "3.7" and python_version < "4.0" base58==2.1.1 ; python_version >= "3.7" and python_version < "4.0"
bech32==1.2.0 ; python_version >= "3.7" and python_version < "4.0" bech32==1.2.0 ; python_version >= "3.7" and python_version < "4.0"
bitstring==3.1.9 ; python_version >= "3.7" and python_version < "4.0" bitstring==3.1.9 ; python_version >= "3.7" and python_version < "4.0"
cashu==0.5.4 ; python_version >= "3.7" and python_version < "4.0" cashu==0.6.0 ; python_version >= "3.7" and python_version < "4.0"
cerberus==1.3.4 ; python_version >= "3.7" and python_version < "4.0" cerberus==1.3.4 ; python_version >= "3.7" and python_version < "4.0"
certifi==2022.9.24 ; python_version >= "3.7" and python_version < "4.0" certifi==2022.9.24 ; python_version >= "3.7" and python_version < "4.0"
cffi==1.15.1 ; python_version >= "3.7" and python_version < "4.0" cffi==1.15.1 ; python_version >= "3.7" and python_version < "4.0"

View file

@ -46,11 +46,11 @@ async def test_get_wallet_no_redirect(client):
i += 1 i += 1
# check GET /wallet: wrong user, expect 204 # check GET /wallet: wrong user, expect 400
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_wallet_with_nonexistent_user(client): async def test_get_wallet_with_nonexistent_user(client):
response = await client.get("wallet", params={"usr": "1"}) response = await client.get("wallet", params={"usr": "1"})
assert response.status_code == 204, ( assert response.status_code == 400, (
str(response.url) + " " + str(response.status_code) str(response.url) + " " + str(response.status_code)
) )
@ -91,11 +91,11 @@ async def test_get_wallet_with_user_and_wallet(client, to_user, to_wallet):
) )
# check GET /wallet: wrong wallet and user, expect 204 # check GET /wallet: wrong wallet and user, expect 400
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_wallet_with_user_and_wrong_wallet(client, to_user): async def test_get_wallet_with_user_and_wrong_wallet(client, to_user):
response = await client.get("wallet", params={"usr": to_user.id, "wal": "1"}) response = await client.get("wallet", params={"usr": to_user.id, "wal": "1"})
assert response.status_code == 204, ( assert response.status_code == 400, (
str(response.url) + " " + str(response.status_code) str(response.url) + " " + str(response.status_code)
) )
@ -109,20 +109,20 @@ async def test_get_extensions(client, to_user):
) )
# check GET /extensions: extensions list wrong user, expect 204 # check GET /extensions: extensions list wrong user, expect 400
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_extensions_wrong_user(client, to_user): async def test_get_extensions_wrong_user(client, to_user):
response = await client.get("extensions", params={"usr": "1"}) response = await client.get("extensions", params={"usr": "1"})
assert response.status_code == 204, ( assert response.status_code == 400, (
str(response.url) + " " + str(response.status_code) str(response.url) + " " + str(response.status_code)
) )
# check GET /extensions: no user given, expect code 204 no content # check GET /extensions: no user given, expect code 400 bad request
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_extensions_no_user(client): async def test_get_extensions_no_user(client):
response = await client.get("extensions") response = await client.get("extensions")
assert response.status_code == 204, ( # no content assert response.status_code == 400, ( # bad request
str(response.url) + " " + str(response.status_code) str(response.url) + " " + str(response.status_code)
) )

Binary file not shown.

View file

@ -61,21 +61,21 @@ async def test_endpoints_inkey(client, inkey_headers_to):
@pytest.mark.asyncio @pytest.mark.asyncio
@pytest.mark.skipif(is_fake, reason="this test is only passes with regtest") @pytest.mark.skipif(is_fake, reason="this test is only passes with regtest")
async def test_endpoints_adminkey_nocontent(client, adminkey_headers_to): async def test_endpoints_adminkey_badrequest(client, adminkey_headers_to):
response = await client.post("/boltz/api/v1/swap", headers=adminkey_headers_to) response = await client.post("/boltz/api/v1/swap", headers=adminkey_headers_to)
assert response.status_code == 204 assert response.status_code == 400
response = await client.post( response = await client.post(
"/boltz/api/v1/swap/reverse", headers=adminkey_headers_to "/boltz/api/v1/swap/reverse", headers=adminkey_headers_to
) )
assert response.status_code == 204 assert response.status_code == 400
response = await client.post( response = await client.post(
"/boltz/api/v1/swap/refund", headers=adminkey_headers_to "/boltz/api/v1/swap/refund", headers=adminkey_headers_to
) )
assert response.status_code == 204 assert response.status_code == 400
response = await client.post( response = await client.post(
"/boltz/api/v1/swap/status", headers=adminkey_headers_to "/boltz/api/v1/swap/status", headers=adminkey_headers_to
) )
assert response.status_code == 204 assert response.status_code == 400
@pytest.mark.asyncio @pytest.mark.asyncio