[CHORE] string formatting default length 88 (#1887)

* [CHORE] string formatting default length 88

uses blacks default off 88 and enabled autostringformatting

* formatting

* nitpicks jackstar

fix
This commit is contained in:
dni ⚡ 2023-08-24 11:26:09 +02:00 committed by GitHub
parent 2ab18544c3
commit 4e6f229db2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 245 additions and 160 deletions

View file

@ -17,14 +17,14 @@ repos:
rev: 23.7.0 rev: 23.7.0
hooks: hooks:
- id: black - id: black
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.0.283
hooks:
- id: ruff
args: [ --fix, --exit-non-zero-on-fix ]
- repo: https://github.com/pre-commit/mirrors-prettier - repo: https://github.com/pre-commit/mirrors-prettier
rev: '50c5478ed9e10bf360335449280cf2a67f4edb7a' rev: '50c5478ed9e10bf360335449280cf2a67f4edb7a'
hooks: hooks:
- id: prettier - id: prettier
types_or: [css, javascript, html, json] types_or: [css, javascript, html, json]
args: ['lnbits'] args: ['lnbits']
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.0.283
hooks:
- id: ruff
args: [ --fix, --exit-non-zero-on-fix ]

View file

@ -413,9 +413,11 @@ def get_db_vendor_name():
return ( return (
"PostgreSQL" "PostgreSQL"
if db_url and db_url.startswith("postgres://") if db_url and db_url.startswith("postgres://")
else "CockroachDB" else (
if db_url and db_url.startswith("cockroachdb://") "CockroachDB"
else "SQLite" if db_url and db_url.startswith("cockroachdb://")
else "SQLite"
)
) )

View file

@ -61,7 +61,8 @@ async def migrate_databases():
) )
elif conn.type in {POSTGRES, COCKROACH}: elif conn.type in {POSTGRES, COCKROACH}:
exists = await conn.fetchone( exists = await conn.fetchone(
"SELECT * FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'dbversions'" "SELECT * FROM information_schema.tables WHERE table_schema = 'public'"
" AND table_name = 'dbversions'"
) )
if not exists: if not exists:

View file

@ -58,7 +58,9 @@ async def get_user(user_id: str, conn: Optional[Connection] = None) -> Optional[
) )
wallets = await (conn or db).fetchall( wallets = await (conn or db).fetchall(
""" """
SELECT *, COALESCE((SELECT balance FROM balances WHERE wallet = wallets.id), 0) AS balance_msat SELECT *, COALESCE((
SELECT balance FROM balances WHERE wallet = wallets.id
), 0) AS balance_msat
FROM wallets FROM wallets
WHERE "user" = ? WHERE "user" = ?
""", """,
@ -89,9 +91,9 @@ async def add_installed_extension(
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> None: ) -> None:
meta = { meta = {
"installed_release": dict(ext.installed_release) "installed_release": (
if ext.installed_release dict(ext.installed_release) if ext.installed_release else None
else None, ),
"dependencies": ext.dependencies, "dependencies": ext.dependencies,
} }
@ -99,9 +101,11 @@ async def add_installed_extension(
await (conn or db).execute( await (conn or db).execute(
""" """
INSERT INTO installed_extensions (id, version, name, short_description, icon, stars, meta) VALUES (?, ?, ?, ?, ?, ?, ?) INSERT INTO installed_extensions
ON CONFLICT (id) DO (id, version, name, short_description, icon, stars, meta)
UPDATE SET (version, name, active, short_description, icon, stars, meta) = (?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT (id) DO UPDATE SET
(version, name, active, short_description, icon, stars, meta) =
(?, ?, ?, ?, ?, ?, ?)
""", """,
( (
ext.id, ext.id,
@ -270,9 +274,8 @@ async def get_wallet(
) -> Optional[Wallet]: ) -> Optional[Wallet]:
row = await (conn or db).fetchone( row = await (conn or db).fetchone(
""" """
SELECT *, COALESCE((SELECT balance FROM balances WHERE wallet = wallets.id), 0) AS balance_msat SELECT *, COALESCE((SELECT balance FROM balances WHERE wallet = wallets.id), 0)
FROM wallets AS balance_msat FROM wallets WHERE id = ?
WHERE id = ?
""", """,
(wallet_id,), (wallet_id,),
) )
@ -287,9 +290,8 @@ async def get_wallet_for_key(
) -> Optional[Wallet]: ) -> Optional[Wallet]:
row = await (conn or db).fetchone( row = await (conn or db).fetchone(
""" """
SELECT *, COALESCE((SELECT balance FROM balances WHERE wallet = wallets.id), 0) AS balance_msat SELECT *, COALESCE((SELECT balance FROM balances WHERE wallet = wallets.id), 0)
FROM wallets AS balance_msat FROM wallets WHERE adminkey = ? OR inkey = ?
WHERE adminkey = ? OR inkey = ?
""", """,
(key, key), (key, key),
) )
@ -544,9 +546,11 @@ async def create_payment(
pending, pending,
memo, memo,
fee, fee,
json.dumps(extra) (
if extra and extra != {} and type(extra) is dict json.dumps(extra)
else None, if extra and extra != {} and type(extra) is dict
else None
),
webhook, webhook,
db.datetime_to_timestamp(expiration_date), db.datetime_to_timestamp(expiration_date),
), ),
@ -608,7 +612,8 @@ async def update_payment_extra(
) -> None: ) -> None:
""" """
Only update the `extra` field for the payment. Only update the `extra` field for the payment.
Old values in the `extra` JSON object will be kept unless the new `extra` overwrites them. Old values in the `extra` JSON object will be kept
unless the new `extra` overwrites them.
""" """
amount_clause = "AND amount < 0" if outgoing else "AND amount > 0" amount_clause = "AND amount < 0" if outgoing else "AND amount > 0"
@ -662,7 +667,10 @@ async def check_internal(
async def check_internal_pending( async def check_internal_pending(
payment_hash: str, conn: Optional[Connection] = None payment_hash: str, conn: Optional[Connection] = None
) -> bool: ) -> bool:
"""Returns False if the internal payment is not pending anymore (and thus paid), otherwise True""" """
Returns False if the internal payment is not pending anymore
(and thus paid), otherwise True
"""
row = await (conn or db).fetchone( row = await (conn or db).fetchone(
""" """
SELECT pending FROM apipayments SELECT pending FROM apipayments

View file

@ -51,7 +51,8 @@ async def stop_extension_background_work(ext_id: str, user: str):
""" """
Stop background work for extension (like asyncio.Tasks, WebSockets, etc). Stop background work for extension (like asyncio.Tasks, WebSockets, etc).
Extensions SHOULD expose a DELETE enpoint at the root level of their API. Extensions SHOULD expose a DELETE enpoint at the root level of their API.
This function tries first to call the endpoint using `http` and if if fails it tries using `https`. This function tries first to call the endpoint using `http`
and if it fails it tries using `https`.
""" """
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
try: try:

View file

@ -239,7 +239,8 @@ async def m007_set_invoice_expiries(db):
invoice.date + invoice.expiry invoice.date + invoice.expiry
) )
logger.info( logger.info(
f"Migration: {i+1}/{len(rows)} setting expiry of invoice {invoice.payment_hash} to {expiration_date}" f"Migration: {i+1}/{len(rows)} setting expiry of invoice"
f" {invoice.payment_hash} to {expiration_date}"
) )
await db.execute( await db.execute(
""" """

View file

@ -115,16 +115,17 @@ async def pay_invoice(
) -> str: ) -> str:
""" """
Pay a Lightning invoice. Pay a Lightning invoice.
First, we create a temporary payment in the database with fees set to the reserve fee. First, we create a temporary payment in the database with fees set to the reserve
We then check whether the balance of the payer would go negative. fee. We then check whether the balance of the payer would go negative.
We then attempt to pay the invoice through the backend. We then attempt to pay the invoice through the backend. If the payment is
If the payment is successful, we update the payment in the database with the payment details. successful, we update the payment in the database with the payment details.
If the payment is unsuccessful, we delete the temporary payment. If the payment is unsuccessful, we delete the temporary payment.
If the payment is still in flight, we hope that some other process will regularly check for the payment. If the payment is still in flight, we hope that some other process
will regularly check for the payment.
""" """
invoice = bolt11.decode(payment_request) invoice = bolt11.decode(payment_request)
fee_reserve_msat = fee_reserve(invoice.amount_msat) fee_reserve_msat = fee_reserve(invoice.amount_msat)
async with (db.reuse_conn(conn) if conn else db.connect()) as conn: async with db.reuse_conn(conn) if conn else db.connect() as conn:
temp_id = invoice.payment_hash temp_id = invoice.payment_hash
internal_id = f"internal_{invoice.payment_hash}" internal_id = f"internal_{invoice.payment_hash}"
@ -151,11 +152,13 @@ async def pay_invoice(
extra=extra, extra=extra,
) )
# we check if an internal invoice exists that has already been paid (not pending anymore) # we check if an internal invoice exists that has already been paid
# (not pending anymore)
if not await check_internal_pending(invoice.payment_hash, conn=conn): if not await check_internal_pending(invoice.payment_hash, conn=conn):
raise PaymentFailure("Internal invoice already paid.") raise PaymentFailure("Internal invoice already paid.")
# check_internal() returns the checking_id of the invoice we're waiting for (pending only) # check_internal() returns the checking_id of the invoice we're waiting for
# (pending only)
internal_checking_id = await check_internal(invoice.payment_hash, conn=conn) internal_checking_id = await check_internal(invoice.payment_hash, conn=conn)
if internal_checking_id: if internal_checking_id:
# perform additional checks on the internal payment # perform additional checks on the internal payment
@ -202,7 +205,8 @@ async def pay_invoice(
logger.debug("balance is too low, deleting temporary payment") logger.debug("balance is too low, deleting temporary payment")
if not internal_checking_id and wallet.balance_msat > -fee_reserve_msat: if not internal_checking_id and wallet.balance_msat > -fee_reserve_msat:
raise PaymentFailure( raise PaymentFailure(
f"You must reserve at least ({round(fee_reserve_msat/1000)} sat) to cover potential routing fees." f"You must reserve at least ({round(fee_reserve_msat/1000)} sat) to"
" cover potential routing fees."
) )
raise PermissionError("Insufficient balance.") raise PermissionError("Insufficient balance.")
@ -232,7 +236,8 @@ async def pay_invoice(
if payment.checking_id and payment.checking_id != temp_id: if payment.checking_id and payment.checking_id != temp_id:
logger.warning( logger.warning(
f"backend sent unexpected checking_id (expected: {temp_id} got: {payment.checking_id})" f"backend sent unexpected checking_id (expected: {temp_id} got:"
f" {payment.checking_id})"
) )
logger.debug(f"backend: pay_invoice finished {temp_id}") logger.debug(f"backend: pay_invoice finished {temp_id}")
@ -267,7 +272,8 @@ async def pay_invoice(
) )
else: else:
logger.warning( logger.warning(
f"didn't receive checking_id from backend, payment may be stuck in database: {temp_id}" "didn't receive checking_id from backend, payment may be stuck in"
f" database: {temp_id}"
) )
return invoice.payment_hash return invoice.payment_hash
@ -301,7 +307,8 @@ async def redeem_lnurl_withdraw(
) )
except Exception: except Exception:
logger.warning( logger.warning(
f"failed to create invoice on redeem_lnurl_withdraw from {lnurl}. params: {res}" f"failed to create invoice on redeem_lnurl_withdraw "
f"from {lnurl}. params: {res}"
) )
return None return None
@ -420,7 +427,8 @@ async def check_transaction_status(
return status return status
# WARN: this same value must be used for balance check and passed to WALLET.pay_invoice(), it may cause a vulnerability if the values differ # WARN: this same value must be used for balance check and passed to
# WALLET.pay_invoice(), it may cause a vulnerability if the values differ
def fee_reserve(amount_msat: int) -> int: def fee_reserve(amount_msat: int) -> int:
reserve_min = settings.lnbits_reserve_fee_min reserve_min = settings.lnbits_reserve_fee_min
reserve_percent = settings.lnbits_reserve_fee_percent reserve_percent = settings.lnbits_reserve_fee_percent

View file

@ -48,7 +48,8 @@ async def killswitch_task():
await switch_to_voidwallet() await switch_to_voidwallet()
except (httpx.ConnectError, httpx.RequestError): except (httpx.ConnectError, httpx.RequestError):
logger.error( logger.error(
f"Cannot fetch lnbits status manifest. {settings.lnbits_status_manifest}" "Cannot fetch lnbits status manifest."
f" {settings.lnbits_status_manifest}"
) )
await asyncio.sleep(settings.lnbits_killswitch_interval * 60) await asyncio.sleep(settings.lnbits_killswitch_interval * 60)
@ -80,8 +81,8 @@ async def watchdog_task():
def register_task_listeners(): def register_task_listeners():
""" """
Registers an invoice listener queue for the core tasks. Registers an invoice listener queue for the core tasks. Incoming payments in this
Incoming payaments in this queue will eventually trigger the signals sent to all other extensions queue will eventually trigger the signals sent to all other extensions
and fulfill other core tasks such as dispatching webhooks. and fulfill other core tasks such as dispatching webhooks.
""" """
invoice_paid_queue = asyncio.Queue(5) invoice_paid_queue = asyncio.Queue(5)
@ -93,7 +94,8 @@ def register_task_listeners():
async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue): async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue):
""" """
This worker dispatches events to all extensions, dispatches webhooks and balance notifys. This worker dispatches events to all extensions,
dispatches webhooks and balance notifys.
""" """
while True: while True:
payment = await invoice_paid_queue.get() payment = await invoice_paid_queue.get()
@ -135,11 +137,15 @@ async def dispatch_webhook(payment: Payment):
""" """
Dispatches the webhook to the webhook url. Dispatches the webhook to the webhook url.
""" """
logger.debug("sending webhook", payment.webhook)
if not payment.webhook:
return await mark_webhook_sent(payment, -1)
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
data = payment.dict() data = payment.dict()
try: try:
logger.debug("sending webhook", payment.webhook) r = await client.post(payment.webhook, json=data, timeout=40)
r = await client.post(payment.webhook, json=data, timeout=40) # type: ignore
await mark_webhook_sent(payment, r.status_code) await mark_webhook_sent(payment, r.status_code)
except (httpx.ConnectError, httpx.RequestError): except (httpx.ConnectError, httpx.RequestError):
await mark_webhook_sent(payment, -1) await mark_webhook_sent(payment, -1)

View file

@ -126,10 +126,10 @@ async def api_download_backup() -> FileResponse:
p = urlparse(db_url) p = urlparse(db_url)
command = ( command = (
f"pg_dump --host={p.hostname} " f"pg_dump --host={p.hostname} "
f'--dbname={p.path.replace("/", "")} ' f"--dbname={p.path.replace('/', '')} "
f"--username={p.username} " f"--username={p.username} "
f"--no-password " "--no-password "
f"--format=c " "--format=c "
f"--file={pg_backup_filename}" f"--file={pg_backup_filename}"
) )
proc = Popen( proc = Popen(

View file

@ -234,8 +234,9 @@ async def api_payments_create_invoice(data: CreateInvoice, wallet: Wallet):
internal=data.internal, internal=data.internal,
conn=conn, conn=conn,
) )
# NOTE: we get the checking_id with a seperate query because create_invoice does not return it # NOTE: we get the checking_id with a seperate query because create_invoice
# and it would be a big hustle to change its return type (used across extensions) # does not return it and it would be a big hustle to change its return type
# (used across extensions)
payment_db = await get_standalone_payment(payment_hash, conn=conn) payment_db = await get_standalone_payment(payment_hash, conn=conn)
assert payment_db is not None, "payment not found" assert payment_db is not None, "payment not found"
checking_id = payment_db.checking_id checking_id = payment_db.checking_id
@ -309,12 +310,13 @@ async def api_payments_pay_invoice(bolt11: str, wallet: Wallet):
"/api/v1/payments", "/api/v1/payments",
summary="Create or pay an invoice", summary="Create or pay an invoice",
description=""" description="""
This endpoint can be used both to generate and pay a BOLT11 invoice. This endpoint can be used both to generate and pay a BOLT11 invoice.
To generate a new invoice for receiving funds into the authorized account, To generate a new invoice for receiving funds into the authorized account,
specify at least the first four fields in the POST body: `out: false`, `amount`, `unit`, and `memo`. specify at least the first four fields in the POST body: `out: false`,
To pay an arbitrary invoice from the funds already in the authorized account, `amount`, `unit`, and `memo`. To pay an arbitrary invoice from the funds
specify `out: true` and use the `bolt11` field to supply the BOLT11 invoice to be paid. already in the authorized account, specify `out: true` and use the `bolt11`
""", field to supply the BOLT11 invoice to be paid.
""",
status_code=HTTPStatus.CREATED, status_code=HTTPStatus.CREATED,
) )
async def api_payments_create( async def api_payments_create(
@ -379,8 +381,10 @@ async def api_payments_pay_lnurl(
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, status_code=HTTPStatus.BAD_REQUEST,
detail=( detail=(
f"{domain} returned an invalid invoice. Expected {data.amount} msat, " (
f"got {invoice.amount_msat}.", f"{domain} returned an invalid invoice. Expected"
f" {data.amount} msat, got {invoice.amount_msat}."
),
), ),
) )
@ -388,8 +392,10 @@ async def api_payments_pay_lnurl(
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, status_code=HTTPStatus.BAD_REQUEST,
detail=( detail=(
f"{domain} returned an invalid invoice. Expected description_hash == " (
f"{data.description_hash}, got {invoice.description_hash}.", f"{domain} returned an invalid invoice. Expected description_hash"
f" == {data.description_hash}, got {invoice.description_hash}."
),
), ),
) )

View file

@ -132,12 +132,12 @@ async def extensions_install(
"isAvailable": ext.id in all_extensions, "isAvailable": ext.id in all_extensions,
"isAdminOnly": ext.id in settings.lnbits_admin_extensions, "isAdminOnly": ext.id in settings.lnbits_admin_extensions,
"isActive": ext.id not in inactive_extensions, "isActive": ext.id not in inactive_extensions,
"latestRelease": dict(ext.latest_release) "latestRelease": (
if ext.latest_release dict(ext.latest_release) if ext.latest_release else None
else None, ),
"installedRelease": dict(ext.installed_release) "installedRelease": (
if ext.installed_release dict(ext.installed_release) if ext.installed_release else None
else None, ),
}, },
installable_exts, installable_exts,
) )
@ -160,13 +160,13 @@ async def extensions_install(
"/wallet", "/wallet",
response_class=HTMLResponse, response_class=HTMLResponse,
description=""" description="""
Args: just **wallet_name**: create a new user, then create a new wallet
for user with wallet_name
just **wallet_name**: create a new user, then create a new wallet for user with wallet_name<br> just **user_id**: return the first user wallet or create one if none found
just **user_id**: return the first user wallet or create one if none found (with default wallet_name)<br> (with default wallet_name)
**user_id** and **wallet_name**: create a new wallet for user with wallet_name<br> **user_id** and **wallet_name**: create a new wallet for user with wallet_name
**user_id** and **wallet_id**: return that wallet if user is the owner<br> **user_id** and **wallet_id**: return that wallet if user is the owner
nothing: create everything<br> nothing: create everything
""", """,
) )
async def wallet( async def wallet(
@ -210,7 +210,8 @@ async def wallet(
else: else:
wallet = await create_wallet(user_id=user.id, wallet_name=wallet_name) wallet = await create_wallet(user_id=user.id, wallet_name=wallet_name)
logger.info( logger.info(
f"Created new wallet {wallet_name if wallet_name else '(no name)'} for user {user.id}" f"Created new wallet {wallet_name if wallet_name else '(no name)'} for"
f" user {user.id}"
) )
return RedirectResponse( return RedirectResponse(
@ -219,7 +220,9 @@ async def wallet(
) )
logger.debug( logger.debug(
f"Access {'user '+ user.id + ' ' if user else ''} {'wallet ' + wallet_name if wallet_name else ''}" "Access "
f"{'user '+ user.id + ' ' if user else ''} "
f"{'wallet ' + wallet_name if wallet_name else ''}"
) )
userwallet = user.get_wallet(wallet_id) userwallet = user.get_wallet(wallet_id)
if not userwallet: if not userwallet:
@ -255,7 +258,9 @@ async def lnurl_full_withdraw(request: Request):
"k1": "0", "k1": "0",
"minWithdrawable": 1000 if wallet.withdrawable_balance else 0, "minWithdrawable": 1000 if wallet.withdrawable_balance else 0,
"maxWithdrawable": wallet.withdrawable_balance, "maxWithdrawable": wallet.withdrawable_balance,
"defaultDescription": f"{settings.lnbits_site_title} balance withdraw from {wallet.id[0:5]}", "defaultDescription": (
f"{settings.lnbits_site_title} balance withdraw from {wallet.id[0:5]}"
),
"balanceCheck": url_for("/withdraw", external=True, usr=user.id, wal=wallet.id), "balanceCheck": url_for("/withdraw", external=True, usr=user.id, wal=wallet.id),
} }
@ -362,9 +367,11 @@ async def manifest(usr: str):
"name": settings.lnbits_site_title + " Wallet", "name": settings.lnbits_site_title + " Wallet",
"icons": [ "icons": [
{ {
"src": settings.lnbits_custom_logo "src": (
if settings.lnbits_custom_logo settings.lnbits_custom_logo
else "https://cdn.jsdelivr.net/gh/lnbits/lnbits@0.3.0/docs/logos/lnbits.png", if settings.lnbits_custom_logo
else "https://cdn.jsdelivr.net/gh/lnbits/lnbits@0.3.0/docs/logos/lnbits.png"
),
"type": "image/png", "type": "image/png",
"sizes": "900x900", "sizes": "900x900",
} }

View file

@ -421,8 +421,8 @@ class Filters(BaseModel, Generic[TFilterModel]):
Generic helper class for filtering and sorting data. Generic helper class for filtering and sorting data.
For usage in an api endpoint, use the `parse_filters` dependency. For usage in an api endpoint, use the `parse_filters` dependency.
When constructing this class manually always make sure to pass a model so that the values can be validated. When constructing this class manually always make sure to pass a model so that
Otherwise, make sure to validate the inputs manually. the values can be validated. Otherwise, make sure to validate the inputs manually.
""" """
filters: List[Filter[TFilterModel]] = [] filters: List[Filter[TFilterModel]] = []

View file

@ -49,16 +49,16 @@ class KeyChecker(SecurityBase):
if self._api_key if self._api_key
else request.headers.get("X-API-KEY") or request.query_params["api-key"] else request.headers.get("X-API-KEY") or request.query_params["api-key"]
) )
# FIXME: Find another way to validate the key. A fetch from DB should be avoided here. # FIXME: Find another way to validate the key. A fetch from DB should be
# Also, we should not return the wallet here - thats silly. # avoided here. Also, we should not return the wallet here - thats
# Possibly store it in a Redis DB # silly. Possibly store it in a Redis DB
self.wallet = await get_wallet_for_key(key_value, self._key_type) # type: ignore wallet = await get_wallet_for_key(key_value, self._key_type)
if not self.wallet: self.wallet = wallet # type: ignore
if not wallet:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.UNAUTHORIZED, status_code=HTTPStatus.UNAUTHORIZED,
detail="Invalid key or expired key.", detail="Invalid key or expired key.",
) )
except KeyError: except KeyError:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail="`X-API-KEY` header missing." status_code=HTTPStatus.BAD_REQUEST, detail="`X-API-KEY` header missing."
@ -156,7 +156,8 @@ async def get_key_type(
if exc.status_code == HTTPStatus.BAD_REQUEST: if exc.status_code == HTTPStatus.BAD_REQUEST:
raise raise
elif exc.status_code == HTTPStatus.UNAUTHORIZED: elif exc.status_code == HTTPStatus.UNAUTHORIZED:
# we pass this in case it is not an invoice key, nor an admin key, and then return NOT_FOUND at the end of this block # we pass this in case it is not an invoice key, nor an admin key,
# and then return NOT_FOUND at the end of this block
pass pass
else: else:
raise raise

View file

@ -426,7 +426,10 @@ class InstallableExtension(BaseModel):
logger.success(f"Extension {self.name} ({self.installed_version}) installed.") logger.success(f"Extension {self.name} ({self.installed_version}) installed.")
def nofiy_upgrade(self) -> None: def nofiy_upgrade(self) -> None:
"""Update the list of upgraded extensions. The middleware will perform redirects based on this""" """
Update the list of upgraded extensions. The middleware will perform
redirects based on this
"""
clean_upgraded_exts = list( clean_upgraded_exts = list(
filter( filter(

View file

@ -93,9 +93,11 @@ def get_current_extension_name() -> str:
def generate_filter_params_openapi(model: Type[FilterModel], keep_optional=False): def generate_filter_params_openapi(model: Type[FilterModel], keep_optional=False):
""" """
Generate openapi documentation for Filters. This is intended to be used along parse_filters (see example) Generate openapi documentation for Filters. This is intended to be used along
parse_filters (see example)
:param model: Filter model :param model: Filter model
:param keep_optional: If false, all parameters will be optional, otherwise inferred from model :param keep_optional: If false, all parameters will be optional,
otherwise inferred from model
""" """
fields = list(model.__fields__.values()) fields = list(model.__fields__.values())
params = [] params = []

View file

@ -18,7 +18,8 @@ from lnbits.settings import settings
class InstalledExtensionMiddleware: class InstalledExtensionMiddleware:
# This middleware class intercepts calls made to the extensions API and: # This middleware class intercepts calls made to the extensions API and:
# - it blocks the calls if the extension has been disabled or uninstalled. # - it blocks the calls if the extension has been disabled or uninstalled.
# - it redirects the calls to the latest version of the extension if the extension has been upgraded. # - it redirects the calls to the latest version of the extension
# if the extension has been upgraded.
# - otherwise it has no effect # - otherwise it has no effect
def __init__(self, app: ASGIApp) -> None: def __init__(self, app: ASGIApp) -> None:
self.app = app self.app = app
@ -89,9 +90,10 @@ class InstalledExtensionMiddleware:
self, headers: List[Any], msg: str, status_code: HTTPStatus self, headers: List[Any], msg: str, status_code: HTTPStatus
) -> Union[HTMLResponse, JSONResponse]: ) -> Union[HTMLResponse, JSONResponse]:
""" """
Build an HTTP response containing the `msg` as HTTP body and the `status_code` as HTTP code. Build an HTTP response containing the `msg` as HTTP body and the `status_code`
If the `accept` HTTP header is present int the request and contains the value of `text/html` as HTTP code. If the `accept` HTTP header is present int the request and
then return an `HTMLResponse`, otherwise return an `JSONResponse`. contains the value of `text/html` then return an `HTMLResponse`,
otherwise return an `JSONResponse`.
""" """
accept_header: str = next( accept_header: str = next(
( (
@ -129,8 +131,8 @@ class CustomGZipMiddleware(GZipMiddleware):
class ExtensionsRedirectMiddleware: class ExtensionsRedirectMiddleware:
# Extensions are allowed to specify redirect paths. # Extensions are allowed to specify redirect paths. A call to a path outside the
# A call to a path outside the scope of the extension can be redirected to one of the extension's endpoints. # scope of the extension can be redirected to one of the extension's endpoints.
# Eg: redirect `GET /.well-known` to `GET /lnurlp/api/v1/well-known` # Eg: redirect `GET /.well-known` to `GET /lnurlp/api/v1/well-known`
def __init__(self, app: ASGIApp) -> None: def __init__(self, app: ASGIApp) -> None:
@ -231,7 +233,8 @@ def add_ip_block_middleware(app: FastAPI):
status_code=403, # Forbidden status_code=403, # Forbidden
content={"detail": "IP is blocked"}, content={"detail": "IP is blocked"},
) )
# this try: except: block is not needed on latest FastAPI (await call_next(request) is enough) # this try: except: block is not needed on latest FastAPI
# (await call_next(request) is enough)
# https://stackoverflow.com/questions/71222144/runtimeerror-no-response-returned-in-fastapi-when-refresh-request # https://stackoverflow.com/questions/71222144/runtimeerror-no-response-returned-in-fastapi-when-refresh-request
# TODO: remove after https://github.com/lnbits/lnbits/pull/1609 is merged # TODO: remove after https://github.com/lnbits/lnbits/pull/1609 is merged
try: try:

View file

@ -46,7 +46,8 @@ def main(
set_cli_settings(host=host, port=port, forwarded_allow_ips=forwarded_allow_ips) set_cli_settings(host=host, port=port, forwarded_allow_ips=forwarded_allow_ips)
# this beautiful beast parses all command line arguments and passes them to the uvicorn server # this beautiful beast parses all command line arguments and
# passes them to the uvicorn server
d = dict() d = dict()
for a in ctx.args: for a in ctx.args:
item = a.split("=") item = a.split("=")

View file

@ -113,7 +113,9 @@ class SecuritySettings(LNbitsSettings):
lnbits_watchdog_interval: int = Field(default=60) lnbits_watchdog_interval: int = Field(default=60)
lnbits_watchdog_delta: int = Field(default=1_000_000) lnbits_watchdog_delta: int = Field(default=1_000_000)
lnbits_status_manifest: str = Field( lnbits_status_manifest: str = Field(
default="https://raw.githubusercontent.com/lnbits/lnbits-status/main/manifest.json" default=(
"https://raw.githubusercontent.com/lnbits/lnbits-status/main/manifest.json"
)
) )
@ -376,7 +378,8 @@ def send_admin_user_to_saas():
logger.success("sent super_user to saas application") logger.success("sent super_user to saas application")
except Exception as e: except Exception as e:
logger.error( logger.error(
f"error sending super_user to saas: {settings.lnbits_saas_callback}. Error: {str(e)}" "error sending super_user to saas:"
f" {settings.lnbits_saas_callback}. Error: {str(e)}"
) )

View file

@ -87,8 +87,8 @@ invoice_listeners: Dict[str, asyncio.Queue] = SseListenersDict("invoice_listener
def register_invoice_listener(send_chan: asyncio.Queue, name: Optional[str] = None): def register_invoice_listener(send_chan: asyncio.Queue, name: Optional[str] = None):
""" """
A method intended for extensions (and core/tasks.py) to call when they want to be notified about A method intended for extensions (and core/tasks.py) to call when they want to be
new invoice payments incoming. Will emit all incoming payments. notified about new invoice payments incoming. Will emit all incoming payments.
""" """
name_unique = f"{name or 'no_name'}_{str(uuid.uuid4())[:8]}" name_unique = f"{name or 'no_name'}_{str(uuid.uuid4())[:8]}"
logger.trace(f"sse: registering invoice listener {name_unique}") logger.trace(f"sse: registering invoice listener {name_unique}")
@ -147,7 +147,8 @@ async def check_pending_payments():
while True: while True:
async with db.connect() as conn: async with db.connect() as conn:
logger.info( logger.info(
f"Task: checking all pending payments (incoming={incoming}, outgoing={outgoing}) of last 15 days" f"Task: checking all pending payments (incoming={incoming},"
f" outgoing={outgoing}) of last 15 days"
) )
start_time = time.time() start_time = time.time()
pending_payments = await get_payments( pending_payments = await get_payments(
@ -163,7 +164,8 @@ async def check_pending_payments():
await payment.check_status(conn=conn) await payment.check_status(conn=conn)
logger.info( logger.info(
f"Task: pending check finished for {len(pending_payments)} payments (took {time.time() - start_time:0.3f} s)" f"Task: pending check finished for {len(pending_payments)} payments"
f" (took {time.time() - start_time:0.3f} s)"
) )
# we delete expired invoices once upon the first pending check # we delete expired invoices once upon the first pending check
if incoming: if incoming:
@ -171,7 +173,8 @@ async def check_pending_payments():
start_time = time.time() start_time = time.time()
await delete_expired_invoices(conn=conn) await delete_expired_invoices(conn=conn)
logger.info( logger.info(
f"Task: expired invoice deletion finished (took {time.time() - start_time:0.3f} s)" "Task: expired invoice deletion finished (took"
f" {time.time() - start_time:0.3f} s)"
) )
# after the first check we will only check outgoing, not incoming # after the first check we will only check outgoing, not incoming

View file

@ -260,12 +260,15 @@ async def btc_price(currency: str) -> float:
rate = float(provider.getter(data, replacements)) rate = float(provider.getter(data, replacements))
await send_channel.put(rate) await send_channel.put(rate)
except ( except (
TypeError, # CoinMate returns HTTPStatus 200 but no data when a currency pair is not found # CoinMate returns HTTPStatus 200 but no data when a pair is not found
KeyError, # Kraken's response dictionary doesn't include keys we look up for TypeError,
# Kraken's response dictionary doesn't include keys we look up for
KeyError,
httpx.ConnectTimeout, httpx.ConnectTimeout,
httpx.ConnectError, httpx.ConnectError,
httpx.ReadTimeout, httpx.ReadTimeout,
httpx.HTTPStatusError, # Some providers throw a 404 when a currency pair is not found # Some providers throw a 404 when a currency pair is not found
httpx.HTTPStatusError,
): ):
await send_channel.put(None) await send_channel.put(None)

View file

@ -55,13 +55,16 @@ class ClicheWallet(Wallet):
description_hash_str = ( description_hash_str = (
description_hash.hex() description_hash.hex()
if description_hash if description_hash
else hashlib.sha256(unhashed_description).hexdigest() else (
if unhashed_description hashlib.sha256(unhashed_description).hexdigest()
else None if unhashed_description
else None
)
) )
ws = create_connection(self.endpoint) ws = create_connection(self.endpoint)
ws.send( ws.send(
f"create-invoice --msatoshi {amount*1000} --description_hash {description_hash_str}" f"create-invoice --msatoshi {amount*1000} --description_hash"
f" {description_hash_str}"
) )
r = ws.recv() r = ws.recv()
else: else:
@ -172,7 +175,8 @@ class ClicheWallet(Wallet):
continue continue
except Exception as exc: except Exception as exc:
logger.error( logger.error(
f"lost connection to cliche's invoices stream: '{exc}', retrying in 5 seconds" f"lost connection to cliche's invoices stream: '{exc}', retrying in"
" 5 seconds"
) )
await asyncio.sleep(5) await asyncio.sleep(5)
continue continue

View file

@ -44,9 +44,8 @@ class CoreLightningWallet(Wallet):
self.ln = LightningRpc(self.rpc) self.ln = LightningRpc(self.rpc)
# check if description_hash is supported (from corelightning>=v0.11.0) # check if description_hash is supported (from corelightning>=v0.11.0)
self.supports_description_hash = ( command = self.ln.help("invoice")["help"][0]["command"] # type: ignore
"deschashonly" in self.ln.help("invoice")["help"][0]["command"] # type: ignore self.supports_description_hash = "deschashonly" in command
)
# check last payindex so we can listen from that point on # check last payindex so we can listen from that point on
self.last_pay_index = 0 self.last_pay_index = 0
@ -79,20 +78,21 @@ class CoreLightningWallet(Wallet):
try: try:
if description_hash and not unhashed_description: if description_hash and not unhashed_description:
raise Unsupported( raise Unsupported(
"'description_hash' unsupported by CoreLightning, provide 'unhashed_description'" "'description_hash' unsupported by CoreLightning, provide"
" 'unhashed_description'"
) )
if unhashed_description and not self.supports_description_hash: if unhashed_description and not self.supports_description_hash:
raise Unsupported("unhashed_description") raise Unsupported("unhashed_description")
r: dict = self.ln.invoice( # type: ignore r: dict = self.ln.invoice( # type: ignore
msatoshi=msat, msatoshi=msat,
label=label, label=label,
description=unhashed_description.decode() description=(
if unhashed_description unhashed_description.decode() if unhashed_description else memo
else memo, ),
exposeprivatechannels=True, exposeprivatechannels=True,
deschashonly=True deschashonly=(
if unhashed_description True if unhashed_description else False
else False, # we can't pass None here ), # we can't pass None here
expiry=kwargs.get("expiry"), expiry=kwargs.get("expiry"),
) )
@ -101,7 +101,10 @@ class CoreLightningWallet(Wallet):
return InvoiceResponse(True, r["payment_hash"], r["bolt11"], "") return InvoiceResponse(True, r["payment_hash"], r["bolt11"], "")
except RpcError as exc: except RpcError as exc:
error_message = f"CoreLightning method '{exc.method}' failed with '{exc.error.get('message') or exc.error}'." # type: ignore error_message = (
f"CoreLightning method '{exc.method}' failed with"
f" '{exc.error.get('message') or exc.error}'." # type: ignore
)
return InvoiceResponse(False, None, None, error_message) return InvoiceResponse(False, None, None, error_message)
except Exception as e: except Exception as e:
return InvoiceResponse(False, None, None, str(e)) return InvoiceResponse(False, None, None, str(e))
@ -114,11 +117,12 @@ class CoreLightningWallet(Wallet):
return PaymentResponse(False, None, None, None, "invoice already paid") return PaymentResponse(False, None, None, None, "invoice already paid")
fee_limit_percent = fee_limit_msat / invoice.amount_msat * 100 fee_limit_percent = fee_limit_msat / invoice.amount_msat * 100
# so fee_limit_percent is applied even on payments with fee < 5000 millisatoshi
# (which is default value of exemptfee)
payload = { payload = {
"bolt11": bolt11, "bolt11": bolt11,
"maxfeepercent": f"{fee_limit_percent:.11}", "maxfeepercent": f"{fee_limit_percent:.11}",
"exemptfee": 0, # so fee_limit_percent is applied even on payments with fee < 5000 millisatoshi (which is default value of exemptfee) "exemptfee": 0,
} }
try: try:
wrapped = async_wrap(_pay_invoice) wrapped = async_wrap(_pay_invoice)
@ -127,7 +131,10 @@ class CoreLightningWallet(Wallet):
try: try:
error_message = exc.error["attempts"][-1]["fail_reason"] # type: ignore error_message = exc.error["attempts"][-1]["fail_reason"] # type: ignore
except Exception: except Exception:
error_message = f"CoreLightning method '{exc.method}' failed with '{exc.error.get('message') or exc.error}'." # type: ignore error_message = (
f"CoreLightning method '{exc.method}' failed with"
f" '{exc.error.get('message') or exc.error}'." # type: ignore
)
return PaymentResponse(False, None, None, None, error_message) return PaymentResponse(False, None, None, None, error_message)
except Exception as exc: except Exception as exc:
return PaymentResponse(False, None, None, None, str(exc)) return PaymentResponse(False, None, None, None, str(exc))
@ -192,6 +199,7 @@ class CoreLightningWallet(Wallet):
yield paid["payment_hash"] yield paid["payment_hash"]
except Exception as exc: except Exception as exc:
logger.error( logger.error(
f"lost connection to corelightning invoices stream: '{exc}', retrying in 5 seconds" f"lost connection to corelightning invoices stream: '{exc}', "
"retrying in 5 seconds"
) )
await asyncio.sleep(5) await asyncio.sleep(5)

View file

@ -225,6 +225,7 @@ class EclairWallet(Wallet):
except Exception as exc: except Exception as exc:
logger.error( logger.error(
f"lost connection to eclair invoices stream: '{exc}', retrying in 5 seconds" f"lost connection to eclair invoices stream: '{exc}'"
"retrying in 5 seconds"
) )
await asyncio.sleep(5) await asyncio.sleep(5)

View file

@ -31,7 +31,8 @@ class FakeWallet(Wallet):
async def status(self) -> StatusResponse: async def status(self) -> StatusResponse:
logger.info( logger.info(
"FakeWallet funding source is for using LNbits as a centralised, stand-alone payment system with brrrrrr." "FakeWallet funding source is for using LNbits as a centralised,"
" stand-alone payment system with brrrrrr."
) )
return StatusResponse(None, 1000000000) return StatusResponse(None, 1000000000)

View file

@ -100,7 +100,8 @@ class LndWallet(Wallet):
def __init__(self): def __init__(self):
if not imports_ok: # pragma: nocover if not imports_ok: # pragma: nocover
raise ImportError( raise ImportError(
"The `grpcio` and `protobuf` library must be installed to use `GRPC LndWallet`. Alternatively try using the LndRESTWallet." "The `grpcio` and `protobuf` library must be installed to use `GRPC"
" LndWallet`. Alternatively try using the LndRESTWallet."
) )
endpoint = settings.lnd_grpc_endpoint endpoint = settings.lnd_grpc_endpoint
@ -305,6 +306,7 @@ class LndWallet(Wallet):
yield checking_id yield checking_id
except Exception as exc: except Exception as exc:
logger.error( logger.error(
f"lost connection to lnd invoices stream: '{exc}', retrying in 5 seconds" f"lost connection to lnd invoices stream: '{exc}', "
"retrying in 5 seconds"
) )
await asyncio.sleep(5) await asyncio.sleep(5)

View file

@ -48,7 +48,8 @@ class LndRestWallet(Wallet):
if not cert: if not cert:
logger.warning( logger.warning(
"no certificate for lndrest provided, this only works if you have a publicly issued certificate" "no certificate for lndrest provided, this only works if you have a"
" publicly issued certificate"
) )
endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
@ -223,6 +224,7 @@ class LndRestWallet(Wallet):
yield payment_hash yield payment_hash
except Exception as exc: except Exception as exc:
logger.error( logger.error(
f"lost connection to lnd invoices stream: '{exc}', retrying in 5 seconds" f"lost connection to lnd invoices stream: '{exc}', retrying in 5"
" seconds"
) )
await asyncio.sleep(5) await asyncio.sleep(5)

View file

@ -50,7 +50,8 @@ class LNPayWallet(Wallet):
data = r.json() data = r.json()
if data["statusType"]["name"] != "active": if data["statusType"]["name"] != "active":
return StatusResponse( return StatusResponse(
f"Wallet {data['user_label']} (data['id']) not active, but {data['statusType']['name']}", f"Wallet {data['user_label']} (data['id']) not active, but"
f" {data['statusType']['name']}",
0, 0,
) )

View file

@ -164,6 +164,7 @@ class LnTipsWallet(Wallet):
# since the backend is expected to drop the connection after 90s # since the backend is expected to drop the connection after 90s
if last_connected is None or time.time() - last_connected < 10: if last_connected is None or time.time() - last_connected < 10:
logger.error( logger.error(
f"lost connection to {self.endpoint}/api/v1/invoicestream, retrying in 5 seconds" f"lost connection to {self.endpoint}/api/v1/invoicestream, retrying"
" in 5 seconds"
) )
await asyncio.sleep(5) await asyncio.sleep(5)

View file

@ -46,7 +46,8 @@ class SparkWallet(Wallet):
async def call(*args, **kwargs): async def call(*args, **kwargs):
if args and kwargs: if args and kwargs:
raise TypeError( raise TypeError(
f"must supply either named arguments or a list of arguments, not both: {args} {kwargs}" "must supply either named arguments or a list of arguments, not"
f" both: {args} {kwargs}"
) )
elif args: elif args:
params = args params = args
@ -161,7 +162,8 @@ class SparkWallet(Wallet):
if len(pays) > 1: if len(pays) > 1:
raise SparkError( raise SparkError(
f"listpays({payment_hash}) returned an unexpected response: {listpays}" f"listpays({payment_hash}) returned an unexpected response:"
f" {listpays}"
) )
if pay["status"] == "failed": if pay["status"] == "failed":

View file

@ -19,10 +19,9 @@ class VoidWallet(Wallet):
async def status(self) -> StatusResponse: async def status(self) -> StatusResponse:
logger.warning( logger.warning(
( "This backend does nothing, it is here just as a placeholder, you must"
"This backend does nothing, it is here just as a placeholder, you must configure an " " configure an actual backend before being able to do anything useful with"
"actual backend before being able to do anything useful with LNbits." " LNbits."
)
) )
return StatusResponse(None, 0) return StatusResponse(None, 0)

View file

@ -103,11 +103,9 @@ testpaths = [
] ]
[tool.black] [tool.black]
# line-length = 150 line-length = 88
# previously experimental-string-processing = true # use upcoming new features
# this should autoformat string poperly but does not work
# preview = true # preview = true
target-versions = ["py39"]
extend-exclude = """( extend-exclude = """(
lnbits/static lnbits/static
| lnbits/extensions | lnbits/extensions
@ -116,14 +114,14 @@ extend-exclude = """(
)""" )"""
[tool.ruff] [tool.ruff]
# Same as Black. # Same as Black. + 10% rule of black
line-length = 150 line-length = 88
# Enable pycodestyle (`E`) and Pyflakes (`F`) codes by default. # Enable pycodestyle (`E`) and Pyflakes (`F`) codes by default.
# (`I`) is for `isort`.
select = ["E", "F", "I"] select = ["E", "F", "I"]
ignore = [ ignore = [
"E402", # Module level import not at top of file "E402", # Module level import not at top of file
"E501", # Line length
] ]
# Allow autofix for all enabled rules (when `--fix`) is provided. # Allow autofix for all enabled rules (when `--fix`) is provided.

View file

@ -210,10 +210,10 @@ async def test_pay_invoice_adminkey(client, invoice, adminkey_headers_from):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_payments(client, from_wallet, adminkey_headers_from): async def test_get_payments(client, from_wallet, adminkey_headers_from):
# Because sqlite only stores timestamps with milliseconds we have to wait a second to ensure # Because sqlite only stores timestamps with milliseconds we have to wait a second
# a different timestamp than previous invoices # to ensure a different timestamp than previous invoices due to this limitation
# due to this limitation both payments (normal and paginated) are tested at the same time as they are almost # both payments (normal and paginated) are tested at the same time as they are
# identical anyways # almost identical anyways
if DB_TYPE == SQLITE: if DB_TYPE == SQLITE:
await asyncio.sleep(1) await asyncio.sleep(1)
ts = time() ts = time()

View file

@ -34,7 +34,10 @@ docker_lightning = f"{docker_cmd} {docker_prefix}-lnd-1-1"
docker_lightning_cli = f"{docker_lightning} lncli --network regtest --rpcserver=lnd-1" docker_lightning_cli = f"{docker_lightning} lncli --network regtest --rpcserver=lnd-1"
docker_bitcoin = f"{docker_cmd} {docker_prefix}-bitcoind-1-1" docker_bitcoin = f"{docker_cmd} {docker_prefix}-bitcoind-1-1"
docker_bitcoin_cli = f"{docker_bitcoin} bitcoin-cli -rpcuser={docker_bitcoin_rpc} -rpcpassword={docker_bitcoin_rpc} -regtest" docker_bitcoin_cli = (
f"{docker_bitcoin} bitcoin-cli"
f" -rpcuser={docker_bitcoin_rpc} -rpcpassword={docker_bitcoin_rpc} -regtest"
)
def run_cmd(cmd: str) -> str: def run_cmd(cmd: str) -> str:

View file

@ -55,7 +55,8 @@ def check_db_versions(sqdb):
version = dbpost[key] version = dbpost[key]
if value != version: if value != version:
raise Exception( raise Exception(
f"sqlite database version ({value}) of {key} doesn't match postgres database version {version}" f"sqlite database version ({value}) of {key} doesn't match postgres"
f" database version {version}"
) )
connection = postgres.connection connection = postgres.connection
@ -174,7 +175,10 @@ parser.add_argument(
dest="sqlite_path", dest="sqlite_path",
const=True, const=True,
nargs="?", nargs="?",
help=f"SQLite DB folder *or* single extension db file to migrate. Default: {sqfolder}", help=(
"SQLite DB folder *or* single extension db file to migrate. Default:"
f" {sqfolder}"
),
default=sqfolder, default=sqfolder,
type=str, type=str,
) )