[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:
parent
2ab18544c3
commit
4e6f229db2
34 changed files with 245 additions and 160 deletions
|
|
@ -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 ]
|
|
||||||
|
|
|
||||||
|
|
@ -413,10 +413,12 @@ 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 (
|
||||||
|
"CockroachDB"
|
||||||
if db_url and db_url.startswith("cockroachdb://")
|
if db_url and db_url.startswith("cockroachdb://")
|
||||||
else "SQLite"
|
else "SQLite"
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def register_async_tasks(app):
|
def register_async_tasks(app):
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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)
|
json.dumps(extra)
|
||||||
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),
|
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
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -311,9 +312,10 @@ async def api_payments_pay_invoice(bolt11: str, wallet: Wallet):
|
||||||
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,
|
||||||
)
|
)
|
||||||
|
|
@ -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}."
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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": (
|
||||||
|
settings.lnbits_custom_logo
|
||||||
if settings.lnbits_custom_logo
|
if settings.lnbits_custom_logo
|
||||||
else "https://cdn.jsdelivr.net/gh/lnbits/lnbits@0.3.0/docs/logos/lnbits.png",
|
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",
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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]] = []
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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 = []
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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("=")
|
||||||
|
|
|
||||||
|
|
@ -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)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
hashlib.sha256(unhashed_description).hexdigest()
|
||||||
if unhashed_description
|
if unhashed_description
|
||||||
else None
|
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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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":
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue