Compare commits

...
12 Commits
Author SHA1 Message Date
dni ⚡ f091ca343a rc4 2024-10-14 13:14:55 +02:00
Vlad Stan 1874a10342 fix: init DB 2024-10-14 13:59:28 +03:00
dni ⚡ c77cb07915 remove update_payment_status/extra/details 2024-10-14 12:34:46 +02:00
dni ⚡ 62d9470010 add dbversion model 2024-10-14 11:19:14 +02:00
dni ⚡ 1c451674cf make get_user better 2024-10-14 10:30:44 +02:00
dni ⚡ 137e716bb8 clean init admin settings 2024-10-14 10:19:21 +02:00
dni ⚡ 75e07b21c7 vlad 2024-10-14 10:06:13 +02:00
dni ⚡ b72a81aa4c fix qrscanner 2024-10-11 13:10:55 +02:00
dni ⚡ 22a16d27f1 fix dynamic chips 2024-10-10 16:06:24 +02:00
dni ⚡ 8843726ae4 qrcode 2024-10-10 13:31:21 +02:00
dni ⚡ fa57b0de3f fix extra null 2024-10-10 13:20:05 +02:00
dni ⚡ 6989d9ab34 fix where 2024-10-10 13:05:56 +02:00
25 changed files with 215 additions and 275 deletions
+2 -2
View File
@@ -17,7 +17,7 @@ from slowapi.util import get_remote_address
from starlette.middleware.sessions import SessionMiddleware from starlette.middleware.sessions import SessionMiddleware
from lnbits.core.crud import ( from lnbits.core.crud import (
get_dbversions, get_db_version,
get_installed_extensions, get_installed_extensions,
update_installed_extension_state, update_installed_extension_state,
) )
@@ -313,7 +313,7 @@ async def restore_installed_extension(app: FastAPI, ext: InstallableExtension):
extension = Extension.from_installable_ext(ext) extension = Extension.from_installable_ext(ext)
register_ext_routes(app, extension) register_ext_routes(app, extension)
current_version = (await get_dbversions()).get(ext.id, 0) current_version = await get_db_version(ext.id)
await migrate_extension_database(ext, current_version) await migrate_extension_database(ext, current_version)
# mount routes for the new version # mount routes for the new version
+8 -6
View File
@@ -17,12 +17,13 @@ from lnbits.core.crud import (
delete_unused_wallets, delete_unused_wallets,
delete_wallet_by_id, delete_wallet_by_id,
delete_wallet_payment, delete_wallet_payment,
get_dbversions, get_db_versions,
get_installed_extension, get_installed_extension,
get_installed_extensions, get_installed_extensions,
get_payment,
get_payments, get_payments,
remove_deleted_wallets, remove_deleted_wallets,
update_payment_status, update_payment,
) )
from lnbits.core.extensions.models import ( from lnbits.core.extensions.models import (
CreateExtension, CreateExtension,
@@ -122,7 +123,7 @@ def database_migrate():
async def db_versions(): async def db_versions():
"""Show current database versions""" """Show current database versions"""
async with core_db.connect() as conn: async with core_db.connect() as conn:
click.echo(await get_dbversions(conn)) click.echo(await get_db_versions(conn))
@db.command("cleanup-wallets") @db.command("cleanup-wallets")
@@ -172,9 +173,10 @@ async def database_delete_wallet_payment(wallet: str, checking_id: str):
async def database_revert_payment(checking_id: str): async def database_revert_payment(checking_id: str):
"""Mark payment as pending""" """Mark payment as pending"""
async with core_db.connect() as conn: async with core_db.connect() as conn:
await update_payment_status( payment = await get_payment(checking_id=checking_id, conn=conn)
status=PaymentState.PENDING, checking_id=checking_id, conn=conn payment.status = PaymentState.PENDING
) await update_payment(payment, conn)
click.echo(f"Payment '{checking_id}' marked as pending.")
@db.command("cleanup-accounts") @db.command("cleanup-accounts")
+48 -102
View File
@@ -1,7 +1,7 @@
import json import json
from datetime import datetime, timezone from datetime import datetime, timezone
from time import time from time import time
from typing import Literal, Optional, Union from typing import Literal, Optional
from uuid import uuid4 from uuid import uuid4
import shortuuid import shortuuid
@@ -25,6 +25,7 @@ from .models import (
AccountFilters, AccountFilters,
AccountOverview, AccountOverview,
CreatePayment, CreatePayment,
DbVersion,
Payment, Payment,
PaymentFilters, PaymentFilters,
PaymentHistoryPoint, PaymentHistoryPoint,
@@ -161,15 +162,18 @@ async def get_account_by_username_or_email(
) )
async def get_user_by_id(
user_id: str, conn: Optional[Connection] = None
) -> Optional[User]:
account = await get_account(user_id, conn)
if not account:
return None
return await get_user(account, conn)
async def get_user( async def get_user(
account_or_id: Union[Account, str], conn: Optional[Connection] = None account: Account, conn: Optional[Connection] = None
) -> Optional[User]: ) -> Optional[User]:
if isinstance(account_or_id, str):
account = await get_account(account_or_id, conn)
if not account:
return None
else:
account = account_or_id
extensions = await get_user_active_extensions_ids(account.id, conn) extensions = await get_user_active_extensions_ids(account.id, conn)
wallets = await get_wallets(account.id, False, conn=conn) wallets = await get_wallets(account.id, False, conn=conn)
return User( return User(
@@ -304,7 +308,7 @@ async def create_user_extension(
async def update_user_extension( async def update_user_extension(
user_extension: UserExtension, conn: Optional[Connection] = None user_extension: UserExtension, conn: Optional[Connection] = None
) -> None: ) -> None:
where = """extension = :extension AND "user" = :user""" where = """WHERE extension = :extension AND "user" = :user"""
await (conn or db).update("extensions", user_extension, where) await (conn or db).update("extensions", user_extension, where)
@@ -476,6 +480,14 @@ async def get_total_balance(conn: Optional[Connection] = None):
# --------------- # ---------------
async def get_payment(checking_id: str, conn: Optional[Connection] = None) -> Payment:
return await (conn or db).fetchone(
"SELECT * FROM apipayments WHERE checking_id = :checking_id",
{"checking_id": checking_id},
Payment,
)
async def get_standalone_payment( async def get_standalone_payment(
checking_id_or_hash: str, checking_id_or_hash: str,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
@@ -712,82 +724,12 @@ async def create_payment(
return new_payment return new_payment
async def update_payment_status( async def update_payment(
checking_id: str, status: PaymentState, conn: Optional[Connection] = None payment: Payment,
) -> None:
await (conn or db).execute(
"UPDATE apipayments SET status = :status WHERE checking_id = :checking_id",
{"status": status.value, "checking_id": checking_id},
)
async def update_payment_details(
checking_id: str,
status: Optional[PaymentState] = None,
fee: Optional[int] = None,
preimage: Optional[str] = None,
new_checking_id: Optional[str] = None,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> None: ) -> None:
set_variables: dict = { await (conn or db).update(
"checking_id": checking_id, "apipayments", payment, "WHERE checking_id = :checking_id"
"new_checking_id": new_checking_id,
"status": status.value if status else None,
"fee": fee,
"preimage": preimage,
}
set_clause: list[str] = []
if new_checking_id is not None:
set_clause.append("checking_id = :checking_id")
if status is not None:
set_clause.append("status = :status")
if fee is not None:
set_clause.append("fee = :fee")
if preimage is not None:
set_clause.append("preimage = :preimage")
await (conn or db).execute(
f"""
UPDATE apipayments SET {', '.join(set_clause)}
WHERE checking_id = :checking_id
""",
set_variables,
)
# TODO: should not be needed use update_payment instead
async def update_payment_extra(
payment_hash: str,
extra: dict,
outgoing: bool = False,
conn: Optional[Connection] = None,
) -> None:
"""
Only update the `extra` field for the payment.
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"
row: dict = await (conn or db).fetchone(
f"""
SELECT payment_hash, extra from apipayments
WHERE payment_hash = :hash {amount_clause}
""",
{"hash": payment_hash},
)
if not row:
return
db_extra = json.loads(row["extra"] if row["extra"] else "{}")
db_extra.update(extra)
await (conn or db).execute(
f"""
UPDATE apipayments SET extra = :extra WHERE payment_hash = :hash {amount_clause}
""",
{"extra": json.dumps(db_extra), "hash": payment_hash},
) )
@@ -869,40 +811,38 @@ async def delete_wallet_payment(
async def check_internal( async def check_internal(
payment_hash: str, conn: Optional[Connection] = None payment_hash: str, conn: Optional[Connection] = None
) -> Optional[str]: ) -> Optional[Payment]:
""" """
Returns the checking_id of the internal payment if it exists, Returns the checking_id of the internal payment if it exists,
otherwise None otherwise None
""" """
row: dict = await (conn or db).fetchone( return await (conn or db).fetchone(
f""" f"""
SELECT checking_id FROM apipayments SELECT * FROM apipayments
WHERE payment_hash = :hash AND status = '{PaymentState.PENDING}' AND amount > 0 WHERE payment_hash = :hash AND status = '{PaymentState.PENDING}' AND amount > 0
""", """,
{"hash": payment_hash}, {"hash": payment_hash},
Payment,
) )
if not row:
return None
else:
return row["checking_id"]
async def check_internal_status( async def is_internal_status_success(
payment_hash: str, conn: Optional[Connection] = None payment_hash: str, conn: Optional[Connection] = None
) -> bool: ) -> bool:
""" """
Returns True if the internal payment was successful Returns True if the internal payment was found and has the given status,
""" """
row: dict = await (conn or db).fetchone( payment = await (conn or db).fetchone(
""" """
SELECT status FROM apipayments SELECT * FROM apipayments
WHERE payment_hash = :payment_hash AND amount > 0 WHERE payment_hash = :payment_hash AND amount > 0
""", """,
{"payment_hash": payment_hash}, {"payment_hash": payment_hash},
Payment,
) )
if not row: if not payment:
return True return True
return row["status"] == PaymentState.SUCCESS.value return payment.status == PaymentState.SUCCESS.value
async def mark_webhook_sent(payment_hash: str, status: int) -> None: async def mark_webhook_sent(payment_hash: str, status: int) -> None:
@@ -982,12 +922,18 @@ async def create_admin_settings(super_user: str, new_settings: dict):
# db versions # db versions
# -------------- # --------------
async def get_dbversions(conn: Optional[Connection] = None) -> dict: async def get_db_version(
result = await (conn or db).execute("SELECT db, version FROM dbversions") ext_id: str, conn: Optional[Connection] = None
_dict = {} ) -> Optional[DbVersion]:
for row in result.mappings().all(): return await (conn or db).fetchone(
_dict[row["db"]] = row["version"] "SELECT * FROM dbversions WHERE db = :ext_id",
return _dict {"ext_id": ext_id},
model=DbVersion,
)
async def get_db_versions(conn: Optional[Connection] = None) -> list[DbVersion]:
return await (conn or db).fetchall("SELECT * FROM dbversions", model=DbVersion)
async def update_migration_version(conn, db_name, version): async def update_migration_version(conn, db_name, version):
+2 -2
View File
@@ -7,7 +7,7 @@ from lnbits.core import core_app_extra
from lnbits.core.crud import ( from lnbits.core.crud import (
create_installed_extension, create_installed_extension,
delete_installed_extension, delete_installed_extension,
get_dbversions, get_db_version,
get_installed_extension, get_installed_extension,
update_installed_extension_state, update_installed_extension_state,
) )
@@ -28,7 +28,7 @@ async def install_extension(ext_info: InstallableExtension) -> Extension:
ext_info.extract_archive() ext_info.extract_archive()
db_version = (await get_dbversions()).get(ext_id, 0) db_version = await get_db_version(ext_id)
await migrate_extension_database(ext_info, db_version) await migrate_extension_database(ext_info, db_version)
await create_installed_extension(ext_info) await create_installed_extension(ext_info)
+20 -8
View File
@@ -1,6 +1,6 @@
import importlib import importlib
import re import re
from typing import Any from typing import Any, Optional
from urllib.parse import urlparse from urllib.parse import urlparse
from uuid import UUID from uuid import UUID
@@ -8,17 +8,20 @@ from loguru import logger
from lnbits.core import migrations as core_migrations from lnbits.core import migrations as core_migrations
from lnbits.core.crud import ( from lnbits.core.crud import (
get_dbversions, get_db_versions,
get_installed_extensions, get_installed_extensions,
update_migration_version, update_migration_version,
) )
from lnbits.core.db import db as core_db from lnbits.core.db import db as core_db
from lnbits.core.extensions.models import InstallableExtension from lnbits.core.extensions.models import InstallableExtension
from lnbits.core.models import DbVersion
from lnbits.db import COCKROACH, POSTGRES, SQLITE, Connection from lnbits.db import COCKROACH, POSTGRES, SQLITE, Connection
from lnbits.settings import settings from lnbits.settings import settings
async def migrate_extension_database(ext: InstallableExtension, current_version: int): async def migrate_extension_database(
ext: InstallableExtension, current_version: Optional[DbVersion] = None
):
try: try:
ext_migrations = importlib.import_module(f"{ext.module_name}.migrations") ext_migrations = importlib.import_module(f"{ext.module_name}.migrations")
@@ -32,14 +35,17 @@ async def migrate_extension_database(ext: InstallableExtension, current_version:
async def run_migration( async def run_migration(
db: Connection, migrations_module: Any, db_name: str, current_version: int db: Connection,
migrations_module: Any,
db_name: str,
current_version: Optional[DbVersion] = None,
): ):
matcher = re.compile(r"^m(\d\d\d)_") matcher = re.compile(r"^m(\d\d\d)_")
for key, migrate in migrations_module.__dict__.items(): for key, migrate in migrations_module.__dict__.items():
match = matcher.match(key) match = matcher.match(key)
if match: if match:
version = int(match.group(1)) version = int(match.group(1))
if version > current_version: if not current_version or version > current_version.version:
logger.debug(f"running migration {db_name}.{version}") logger.debug(f"running migration {db_name}.{version}")
print(f"running migration {db_name}.{version}") print(f"running migration {db_name}.{version}")
await migrate(db) await migrate(db)
@@ -71,7 +77,6 @@ async def load_disabled_extension_list() -> None:
async def migrate_databases(): async def migrate_databases():
"""Creates the necessary databases if they don't exist already; or migrates them.""" """Creates the necessary databases if they don't exist already; or migrates them."""
current_versions = await get_dbversions()
async with core_db.connect() as conn: async with core_db.connect() as conn:
exists = False exists = False
if conn.type == SQLITE: if conn.type == SQLITE:
@@ -87,7 +92,11 @@ async def migrate_databases():
if not exists: if not exists:
await core_migrations.m000_create_migrations_table(conn) await core_migrations.m000_create_migrations_table(conn)
core_version = current_versions.get("core", 0) current_versions = await get_db_versions(conn)
core_version = next(
(v for v in current_versions if v.db == "core"),
DbVersion(db="core", version=0),
)
await run_migration(conn, core_migrations, "core", core_version) await run_migration(conn, core_migrations, "core", core_version)
# here is the first place we can be sure that the # here is the first place we can be sure that the
@@ -95,7 +104,10 @@ async def migrate_databases():
await load_disabled_extension_list() await load_disabled_extension_list()
for ext in await get_installed_extensions(): for ext in await get_installed_extensions():
current_version = current_versions.get(ext.id) current_version = next(
(v for v in current_versions if v.db == ext.id),
DbVersion(db=ext.id, version=0),
)
if current_version is None: if current_version is None:
logger.warning( logger.warning(
f"Extension {ext.id} has no migration version. This should not happen." f"Extension {ext.id} has no migration version. This should not happen."
+2
View File
@@ -562,6 +562,8 @@ async def m023_add_column_column_to_apipayments(db):
await db.execute("ALTER TABLE apipayments RENAME COLUMN wallet TO wallet_id") await db.execute("ALTER TABLE apipayments RENAME COLUMN wallet TO wallet_id")
await db.execute("ALTER TABLE accounts RENAME COLUMN pass TO password_hash") await db.execute("ALTER TABLE accounts RENAME COLUMN pass TO password_hash")
await db.execute("CREATE INDEX by_hash ON apipayments (payment_hash)")
async def m024_drop_pending(db): async def m024_drop_pending(db):
await db.execute("ALTER TABLE apipayments DROP COLUMN pending") await db.execute("ALTER TABLE apipayments DROP COLUMN pending")
+5 -10
View File
@@ -2,7 +2,6 @@ from __future__ import annotations
import hashlib import hashlib
import hmac import hmac
import json
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime, timezone from datetime import datetime, timezone
from enum import Enum from enum import Enum
@@ -27,15 +26,6 @@ from lnbits.wallets.base import (
) )
def json_custom_serialization(_, o):
if isinstance(o, datetime):
return o.isoformat()
raise TypeError(f"Object is not JSON serializable: {o}")
json.JSONEncoder.default = json_custom_serialization # type: ignore[method-assign]
class BaseWallet(BaseModel): class BaseWallet(BaseModel):
id: str id: str
name: str name: str
@@ -479,3 +469,8 @@ class BalanceDelta(BaseModel):
class SimpleStatus(BaseModel): class SimpleStatus(BaseModel):
success: bool success: bool
message: str message: str
class DbVersion(BaseModel):
db: str
version: int
+68 -89
View File
@@ -1,7 +1,6 @@
import asyncio import asyncio
import json import json
import time import time
from datetime import datetime
from io import BytesIO from io import BytesIO
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
@@ -47,7 +46,6 @@ from lnbits.wallets.base import (
from .crud import ( from .crud import (
check_internal, check_internal,
check_internal_status,
create_account, create_account,
create_admin_settings, create_admin_settings,
create_payment, create_payment,
@@ -56,6 +54,7 @@ from .crud import (
get_account_by_email, get_account_by_email,
get_account_by_pubkey, get_account_by_pubkey,
get_account_by_username, get_account_by_username,
get_payment,
get_payments, get_payments,
get_standalone_payment, get_standalone_payment,
get_super_settings, get_super_settings,
@@ -63,9 +62,9 @@ from .crud import (
get_user, get_user,
get_wallet, get_wallet,
get_wallet_payment, get_wallet_payment,
is_internal_status_success,
update_admin_settings, update_admin_settings,
update_payment_details, update_payment,
update_payment_status,
update_super_user, update_super_user,
update_user_extension, update_user_extension,
) )
@@ -247,19 +246,17 @@ async def pay_invoice(
extra=extra, extra=extra,
) )
# we check if an internal invoice exists that has already been paid if await is_internal_status_success(invoice.payment_hash, conn=conn):
# (not pending anymore)
if await check_internal_status(invoice.payment_hash, conn=conn):
raise PaymentError("Internal invoice already paid.", status="failed") raise PaymentError("Internal invoice already paid.", status="failed")
# check_internal() returns the checking_id of the invoice we're waiting for # check_internal() returns the checking_id of the invoice we're waiting for
# (pending only) # (pending only)
internal_checking_id = await check_internal(invoice.payment_hash, conn=conn) internal_payment = await check_internal(invoice.payment_hash, conn=conn)
if internal_checking_id: if internal_payment:
# perform additional checks on the internal payment # perform additional checks on the internal payment
# the payment hash is not enough to make sure that this is the same invoice # the payment hash is not enough to make sure that this is the same invoice
internal_invoice = await get_standalone_payment( internal_invoice = await get_standalone_payment(
internal_checking_id, incoming=True, conn=conn internal_payment.checking_id, incoming=True, conn=conn
) )
assert internal_invoice is not None assert internal_invoice is not None
if ( if (
@@ -293,7 +290,7 @@ async def pay_invoice(
wallet = await get_wallet(wallet_id, conn=conn) wallet = await get_wallet(wallet_id, conn=conn)
assert wallet, "Wallet for balancecheck could not be fetched" assert wallet, "Wallet for balancecheck could not be fetched"
fee_reserve_total_msat = fee_reserve_total(invoice.amount_msat, internal=False) fee_reserve_total_msat = fee_reserve_total(invoice.amount_msat, internal=False)
_check_wallet_balance(wallet, fee_reserve_total_msat, internal_checking_id) _check_wallet_balance(wallet, fee_reserve_total_msat, internal_payment)
if extra and "tag" in extra: if extra and "tag" in extra:
# check if the payment is made for an extension that the user disabled # check if the payment is made for an extension that the user disabled
@@ -301,79 +298,71 @@ async def pay_invoice(
if not status.success: if not status.success:
raise PaymentError(status.message) raise PaymentError(status.message)
if internal_checking_id: if internal_payment:
service_fee_msat = service_fee(invoice.amount_msat, internal=True) service_fee_msat = service_fee(invoice.amount_msat, internal=True)
logger.debug(f"marking temporary payment as not pending {internal_checking_id}") logger.debug(
f"marking temporary payment as not pending {internal_payment.checking_id}"
)
# mark the invoice from the other side as not pending anymore # mark the invoice from the other side as not pending anymore
# so the other side only has access to his new money when we are sure # so the other side only has access to his new money when we are sure
# the payer has enough to deduct from # the payer has enough to deduct from
async with db.connect() as conn: async with db.connect() as conn:
await update_payment_status( internal_payment.status = PaymentState.SUCCESS
checking_id=internal_checking_id, await update_payment(internal_payment, conn=conn)
status=PaymentState.SUCCESS,
conn=conn,
)
await send_payment_notification(wallet, new_payment) await send_payment_notification(wallet, new_payment)
# notify receiver asynchronously # notify receiver asynchronously
from lnbits.tasks import internal_invoice_queue from lnbits.tasks import internal_invoice_queue
logger.debug(f"enqueuing internal invoice {internal_checking_id}") logger.debug(f"enqueuing internal invoice {internal_payment.checking_id}")
await internal_invoice_queue.put(internal_checking_id) await internal_invoice_queue.put(internal_payment.checking_id)
else: else:
fee_reserve_msat = fee_reserve(invoice.amount_msat, internal=False) fee_reserve_msat = fee_reserve(invoice.amount_msat, internal=False)
service_fee_msat = service_fee(invoice.amount_msat, internal=False) service_fee_msat = service_fee(invoice.amount_msat, internal=False)
logger.debug(f"backend: sending payment {temp_id}") logger.debug(f"backend: sending payment {temp_id}")
# actually pay the external invoice # actually pay the external invoice
funding_source = get_funding_source() funding_source = get_funding_source()
payment: PaymentResponse = await funding_source.pay_invoice( payment_response: PaymentResponse = await funding_source.pay_invoice(
payment_request, fee_reserve_msat payment_request, fee_reserve_msat
) )
if payment.checking_id and payment.checking_id != temp_id: if payment_response.checking_id and payment_response.checking_id != temp_id:
logger.warning( logger.warning(
f"backend sent unexpected checking_id (expected: {temp_id} got:" f"backend sent unexpected checking_id (expected: {temp_id} got:"
f" {payment.checking_id})" f" {payment_response.checking_id})"
) )
logger.debug(f"backend: pay_invoice finished {temp_id}, {payment}") logger.debug(f"backend: pay_invoice finished {temp_id}, {payment_response}")
if payment.checking_id and payment.ok is not False: if payment_response.checking_id and payment_response.ok is not False:
# payment.ok can be True (paid) or None (pending)! # payment.ok can be True (paid) or None (pending)!
logger.debug(f"updating payment {temp_id}") logger.debug(f"updating payment {temp_id}")
async with db.connect() as conn: async with db.connect() as conn:
await update_payment_details( payment = await get_payment(temp_id, conn=conn)
checking_id=temp_id, # new checking id
status=( payment.checking_id = payment_response.checking_id
PaymentState.SUCCESS payment.status = (
if payment.ok is True PaymentState.SUCCESS
else PaymentState.PENDING if payment_response.ok is True
), else PaymentState.PENDING
fee=-(
abs(payment.fee_msat if payment.fee_msat else 0)
+ abs(service_fee_msat)
),
preimage=payment.preimage,
new_checking_id=payment.checking_id,
conn=conn,
) )
payment.fee = -(
abs(payment_response.fee_msat or 0) + abs(service_fee_msat)
)
payment.preimage = payment_response.preimage
await update_payment(payment, conn=conn)
wallet = await get_wallet(wallet_id, conn=conn) wallet = await get_wallet(wallet_id, conn=conn)
updated = await get_wallet_payment( if wallet:
wallet_id, payment.checking_id, conn=conn await send_payment_notification(wallet, payment)
) logger.success(f"payment successful {payment_response.checking_id}")
if wallet and updated: elif payment_response.checking_id is None and payment_response.ok is False:
await send_payment_notification(wallet, updated)
logger.success(f"payment successful {payment.checking_id}")
elif payment.checking_id is None and payment.ok is False:
# payment failed # payment failed
logger.debug(f"payment failed {temp_id}, {payment.error_message}") logger.debug(f"payment failed {temp_id}, {payment_response.error_message}")
async with db.connect() as conn: async with db.connect() as conn:
await update_payment_status( payment = await get_payment(temp_id, conn=conn)
checking_id=temp_id, payment.status = PaymentState.FAILED
status=PaymentState.FAILED, await update_payment(payment, conn=conn)
conn=conn,
)
raise PaymentError( raise PaymentError(
f"Payment failed: {payment.error_message}" f"Payment failed: {payment_response.error_message}"
or "Payment failed, but backend didn't give us an error message.", or "Payment failed, but backend didn't give us an error message.",
status="failed", status="failed",
) )
@@ -420,9 +409,8 @@ async def _create_external_payment(
status = await old_payment.check_status() status = await old_payment.check_status()
if status.success: if status.success:
# payment was successful on the fundingsource # payment was successful on the fundingsource
await update_payment_status( old_payment.status = PaymentState.SUCCESS
checking_id=temp_id, status=PaymentState.SUCCESS, conn=conn await update_payment(old_payment, conn=conn)
)
raise PaymentError( raise PaymentError(
"Failed payment was already paid on the fundingsource.", "Failed payment was already paid on the fundingsource.",
status="success", status="success",
@@ -454,11 +442,11 @@ async def _create_external_payment(
def _check_wallet_balance( def _check_wallet_balance(
wallet: Wallet, wallet: Wallet,
fee_reserve_total_msat: int, fee_reserve_total_msat: int,
internal_checking_id: Optional[str] = None, internal_payment: Optional[Payment] = None,
): ):
if wallet.balance_msat < 0: if wallet.balance_msat < 0:
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_total_msat: if not internal_payment and wallet.balance_msat > -fee_reserve_total_msat:
raise PaymentError( raise PaymentError(
f"You must reserve at least ({round(fee_reserve_total_msat/1000)}" f"You must reserve at least ({round(fee_reserve_total_msat/1000)}"
" sat) to cover potential routing fees.", " sat) to cover potential routing fees.",
@@ -717,22 +705,23 @@ async def send_payment_notification(wallet: Wallet, payment: Payment):
async def update_wallet_balance(wallet_id: str, amount: int): async def update_wallet_balance(wallet_id: str, amount: int):
payment_hash, _ = await create_invoice(
wallet_id=wallet_id,
amount=amount,
memo="Admin top up",
internal=True,
)
async with db.connect() as conn: async with db.connect() as conn:
checking_id = await check_internal(payment_hash, conn=conn) payment_hash, _ = await create_invoice(
assert checking_id, "newly created checking_id cannot be retrieved" wallet_id=wallet_id,
await update_payment_status( amount=amount,
checking_id=checking_id, status=PaymentState.SUCCESS, conn=conn memo="Admin top up",
internal=True,
conn=conn,
) )
internal_payment = await check_internal(payment_hash, conn=conn)
assert internal_payment, "newly created checking_id cannot be retrieved"
internal_payment.status = PaymentState.SUCCESS
await update_payment(internal_payment, conn=conn)
# notify receiver asynchronously # notify receiver asynchronously
from lnbits.tasks import internal_invoice_queue from lnbits.tasks import internal_invoice_queue
await internal_invoice_queue.put(checking_id) await internal_invoice_queue.put(internal_payment.checking_id)
async def check_admin_settings(): async def check_admin_settings():
@@ -813,22 +802,16 @@ def update_cached_settings(sets_dict: dict):
async def init_admin_settings(super_user: Optional[str] = None) -> SuperSettings: async def init_admin_settings(super_user: Optional[str] = None) -> SuperSettings:
async def new_account(account_id: str) -> Account:
now = datetime.now()
account = Account(
id=account_id,
extra=UserExtra(provider="env"),
created_at=now,
updated_at=now,
)
await create_account(account)
return account
account = None account = None
if super_user: if super_user:
account = await get_account(super_user) account = await get_account(super_user)
if not account: if not account:
account = await new_account(super_user or uuid4().hex) account_id = super_user or uuid4().hex
account = Account(
id=account_id,
extra=UserExtra(provider="env"),
)
await create_account(account)
await create_wallet(user_id=account.id) await create_wallet(user_id=account.id)
editable_settings = EditableSettings.from_dict(settings.dict()) editable_settings = EditableSettings.from_dict(settings.dict())
@@ -924,12 +907,8 @@ async def update_pending_payments(wallet_id: str):
for payment in pending_payments: for payment in pending_payments:
status = await payment.check_status() status = await payment.check_status()
if status.failed: if status.failed:
await update_payment_status( payment.status = PaymentState.FAILED
checking_id=payment.checking_id, await update_payment(payment)
status=PaymentState.FAILED,
)
elif status.success: elif status.success:
await update_payment_status( payment.status = PaymentState.SUCCESS
checking_id=payment.checking_id, await update_payment(payment)
status=PaymentState.SUCCESS,
)
+4 -4
View File
@@ -621,8 +621,8 @@
<div v-else> <div v-else>
<q-responsive :ratio="1"> <q-responsive :ratio="1">
<qrcode-stream <qrcode-stream
@decode="decodeQR" @detect="decodeQR"
@init="onInitQR" @camera-on="onInitQR"
class="rounded-borders" class="rounded-borders"
></qrcode-stream> ></qrcode-stream>
</q-responsive> </q-responsive>
@@ -645,8 +645,8 @@
<q-card class="q-pa-lg q-pt-xl"> <q-card class="q-pa-lg q-pt-xl">
<div class="text-center q-mb-lg"> <div class="text-center q-mb-lg">
<qrcode-stream <qrcode-stream
@decode="decodeQR" @detect="decodeQR"
@init="onInitQR" @camera-on="onInitQR"
class="rounded-borders" class="rounded-borders"
></qrcode-stream> ></qrcode-stream>
</div> </div>
+2 -2
View File
@@ -40,7 +40,7 @@ from ..crud import (
create_user_extension, create_user_extension,
delete_dbversion, delete_dbversion,
drop_extension_db, drop_extension_db,
get_dbversions, get_db_version,
get_installed_extension, get_installed_extension,
get_installed_extensions, get_installed_extensions,
get_user_extension, get_user_extension,
@@ -474,7 +474,7 @@ async def get_extension_release(org: str, repo: str, tag_name: str):
) )
async def delete_extension_db(ext_id: str): async def delete_extension_db(ext_id: str):
try: try:
db_version = (await get_dbversions()).get(ext_id, None) db_version = await get_db_version(ext_id)
if not db_version: if not db_version:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, status_code=HTTPStatus.BAD_REQUEST,
+8 -5
View File
@@ -23,9 +23,9 @@ from lnbits.wallets import get_funding_source
from ...utils.exchange_rates import allowed_currencies, currencies from ...utils.exchange_rates import allowed_currencies, currencies
from ..crud import ( from ..crud import (
create_wallet, create_wallet,
get_dbversions, get_db_versions,
get_installed_extensions, get_installed_extensions,
get_user, get_user_by_id,
get_wallet, get_wallet,
) )
@@ -104,7 +104,8 @@ async def extensions(request: Request, user: User = Depends(check_user_exists)):
all_ext_ids = [ext.code for ext in Extension.get_valid_extensions()] all_ext_ids = [ext.code for ext in Extension.get_valid_extensions()]
inactive_extensions = [e.id for e in await get_installed_extensions(active=False)] inactive_extensions = [e.id for e in await get_installed_extensions(active=False)]
db_version = await get_dbversions() db_versions = await get_db_versions()
extensions = [ extensions = [
{ {
"id": ext.id, "id": ext.id,
@@ -115,7 +116,9 @@ async def extensions(request: Request, user: User = Depends(check_user_exists)):
"isFeatured": ext.meta.featured if ext.meta else False, "isFeatured": ext.meta.featured if ext.meta else False,
"dependencies": ext.meta.dependencies if ext.meta else "", "dependencies": ext.meta.dependencies if ext.meta else "",
"isInstalled": ext.id in installed_exts_ids, "isInstalled": ext.id in installed_exts_ids,
"hasDatabaseTables": ext.id in db_version, "hasDatabaseTables": next(
(True for version in db_versions if version.db == ext.id), False
),
"isAvailable": ext.id in all_ext_ids, "isAvailable": ext.id in all_ext_ids,
"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,
@@ -229,7 +232,7 @@ async def service_worker(request: Request):
@generic_router.get("/manifest/{usr}.webmanifest") @generic_router.get("/manifest/{usr}.webmanifest")
async def manifest(request: Request, usr: str): async def manifest(request: Request, usr: str):
host = urlparse(str(request.url)).netloc host = urlparse(str(request.url)).netloc
user = await get_user(usr) user = await get_user_by_id(usr)
if not user: if not user:
raise HTTPException(status_code=HTTPStatus.NOT_FOUND) raise HTTPException(status_code=HTTPStatus.NOT_FOUND)
return { return {
+3 -1
View File
@@ -175,7 +175,9 @@ class Connection(Compat):
return dict_to_model(row, model) return dict_to_model(row, model)
return row return row
async def update(self, table_name: str, model: BaseModel, where: str = "id = :id"): async def update(
self, table_name: str, model: BaseModel, where: str = "WHERE id = :id"
):
await self.conn.execute( await self.conn.execute(
text(update_query(table_name, model, where)), model_to_dict(model) text(update_query(table_name, model, where)), model_to_dict(model)
) )
File diff suppressed because one or more lines are too long
+2 -2
View File
File diff suppressed because one or more lines are too long
-4
View File
@@ -526,10 +526,6 @@ video {
word-break: break-word; word-break: break-word;
} }
.q-card--dark, .q-date--dark {
box-shadow: none;
}
.q-card code { .q-card code {
overflow-wrap: break-word; overflow-wrap: break-word;
} }
+1 -1
View File
@@ -278,7 +278,7 @@ window.LNbits = {
preimage: data.preimage, preimage: data.preimage,
payment_hash: data.payment_hash, payment_hash: data.payment_hash,
expiry: data.expiry, expiry: data.expiry,
extra: data.extra ? JSON.parse(data.extra) : {}, extra: data.extra ?? {},
wallet_id: data.wallet_id, wallet_id: data.wallet_id,
webhook: data.webhook, webhook: data.webhook,
webhook_status: data.webhook_status, webhook_status: data.webhook_status,
+3 -4
View File
@@ -409,7 +409,7 @@ window.app.component('lnbits-dynamic-fields', {
data() { data() {
return { return {
formData: null, formData: null,
rules: [val => !!val || 'Field is required'], rules: [val => !!val || 'Field is required']
} }
}, },
methods: { methods: {
@@ -428,7 +428,7 @@ window.app.component('lnbits-dynamic-fields', {
}, },
handleValueChanged() { handleValueChanged() {
this.$emit('update:model-value', this.formData) this.$emit('update:model-value', this.formData)
}, }
}, },
created() { created() {
this.formData = this.buildData(this.options, this.modelValue) this.formData = this.buildData(this.options, this.modelValue)
@@ -450,7 +450,7 @@ window.app.component('lnbits-dynamic-chips', {
if (!this.chip) return if (!this.chip) return
this.chips.push(this.chip) this.chips.push(this.chip)
this.chip = '' this.chip = ''
this.modelValue = this.chips.join(',') this.$emit('update:model-value', this.chips.join(','))
}, },
removeChip(index) { removeChip(index) {
this.chips.splice(index, 1) this.chips.splice(index, 1)
@@ -464,7 +464,6 @@ window.app.component('lnbits-dynamic-chips', {
this.chips = [...this.modelValue] this.chips = [...this.modelValue]
} }
} }
}) })
window.app.component('lnbits-update-balance', { window.app.component('lnbits-update-balance', {
+1 -1
View File
@@ -257,7 +257,7 @@ window.app = Vue.createApp({
}) })
}, },
decodeQR: function (res) { decodeQR: function (res) {
this.parse.data.request = res this.parse.data.request = res[0].rawValue
this.decodeRequest() this.decodeRequest()
this.parse.camera.show = false this.parse.camera.show = false
}, },
File diff suppressed because one or more lines are too long
+11 -19
View File
@@ -20,8 +20,7 @@ from lnbits.core.crud import (
delete_webpush_subscriptions, delete_webpush_subscriptions,
get_payments, get_payments,
get_standalone_payment, get_standalone_payment,
update_payment_details, update_payment,
update_payment_status,
) )
from lnbits.core.models import Payment, PaymentState from lnbits.core.models import Payment, PaymentState
from lnbits.settings import settings from lnbits.settings import settings
@@ -181,17 +180,14 @@ async def check_pending_payments():
status = await payment.check_status() status = await payment.check_status()
prefix = f"payment ({i+1} / {count})" prefix = f"payment ({i+1} / {count})"
if status.failed: if status.failed:
await update_payment_status( payment.status = PaymentState.FAILED
payment.checking_id, status=PaymentState.FAILED await update_payment(payment)
)
logger.debug(f"{prefix} failed {payment.checking_id}") logger.debug(f"{prefix} failed {payment.checking_id}")
elif status.success: elif status.success:
await update_payment_details( payment.fee = status.fee_msat or 0
checking_id=payment.checking_id, payment.preimage = status.preimage
fee=status.fee_msat, payment.status = PaymentState.SUCCESS
preimage=status.preimage, await update_payment(payment)
status=PaymentState.SUCCESS,
)
logger.debug(f"{prefix} success {payment.checking_id}") logger.debug(f"{prefix} success {payment.checking_id}")
else: else:
logger.debug(f"{prefix} pending {payment.checking_id}") logger.debug(f"{prefix} pending {payment.checking_id}")
@@ -211,14 +207,10 @@ async def invoice_callback_dispatcher(checking_id: str, is_internal: bool = Fals
payment = await get_standalone_payment(checking_id, incoming=True) payment = await get_standalone_payment(checking_id, incoming=True)
if payment and payment.is_in: if payment and payment.is_in:
status = await payment.check_status() status = await payment.check_status()
await update_payment_details( payment.fee = status.fee_msat or 0
checking_id=payment.checking_id, payment.preimage = status.preimage
fee=status.fee_msat, payment.status = PaymentState.SUCCESS
preimage=status.preimage, await update_payment(payment)
status=PaymentState.SUCCESS,
)
payment = await get_standalone_payment(checking_id, incoming=True)
assert payment, "updated payment not found"
internal = "internal" if is_internal else "" internal = "internal" if is_internal else ""
logger.success(f"{internal} invoice {checking_id} settled") logger.success(f"{internal} invoice {checking_id} settled")
for name, send_chan in invoice_listeners.items(): for name, send_chan in invoice_listeners.items():
+10 -3
View File
@@ -393,7 +393,7 @@
></q-input> ></q-input>
<div v-else-if="o.type === 'chips'"> <div v-else-if="o.type === 'chips'">
<lnbits-dynamic-chips <lnbits-dynamic-chips
:model-value="formData[o.name]" v-model="formData[o.name]"
@update:model-value="handleValueChanged" @update:model-value="handleValueChanged"
></lnbits-dynamic-chips> ></lnbits-dynamic-chips>
</div> </div>
@@ -489,8 +489,15 @@
<template id="lnbits-qrcode"> <template id="lnbits-qrcode">
<div class="qrcode__wrapper"> <div class="qrcode__wrapper">
<qrcode-vue :value="value" size="350" class="rounded-borders"></qrcode-vue> <qrcode-vue
<img class="qrcode__image" :src="logo" alt="..." /> :value="value"
level="Q"
render-as="svg"
margin="1"
size="350"
class="rounded-borders"
></qrcode-vue>
<img class="qrcode__image" :src="logo" alt="qrcode icon" />
</div> </div>
</template> </template>
+4 -3
View File
@@ -1320,9 +1320,10 @@
} }
}, },
"node_modules/vue-qrcode-reader": { "node_modules/vue-qrcode-reader": {
"version": "5.5.10", "version": "5.5.11",
"resolved": "https://registry.npmjs.org/vue-qrcode-reader/-/vue-qrcode-reader-5.5.10.tgz", "resolved": "https://registry.npmjs.org/vue-qrcode-reader/-/vue-qrcode-reader-5.5.11.tgz",
"integrity": "sha512-lj83FKqRyvo0VLMu49wrLsaHueonfXcwyX9r/GDw0y+myOY5xTfsl75hjBgmmByAxzFSlCPI+CGA9FxYVtRAFQ==", "integrity": "sha512-Ec/bVML1jgxSX+usbgdcXGhOFEFo4EzApCO2CNT1YK0Dcb0Mp7ASygz78RJJs22SU2oI7vz9iJDyr4ucSDTvjQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"barcode-detector": "2.2.2", "barcode-detector": "2.2.2",
"webrtc-adapter": "8.2.3" "webrtc-adapter": "8.2.3"
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "lnbits" name = "lnbits"
version = "1.0.0-rc3" version = "1.0.0-rc4"
description = "LNbits, free and open-source Lightning wallet and accounts system." description = "LNbits, free and open-source Lightning wallet and accounts system."
authors = ["Alan Bits <alan@lnbits.com>"] authors = ["Alan Bits <alan@lnbits.com>"]
readme = "README.md" readme = "README.md"
+5 -2
View File
@@ -20,8 +20,9 @@ from lnbits.core.crud import (
delete_account, delete_account,
get_account, get_account,
get_account_by_username, get_account_by_username,
get_payment,
get_user, get_user,
update_payment_status, update_payment,
) )
from lnbits.core.models import Account, CreateInvoice, PaymentState, User from lnbits.core.models import Account, CreateInvoice, PaymentState, User
from lnbits.core.services import create_user_account, update_wallet_balance from lnbits.core.services import create_user_account, update_wallet_balance
@@ -250,7 +251,9 @@ async def fake_payments(client, adminkey_headers_from):
assert response.is_success assert response.is_success
data = response.json() data = response.json()
assert data["checking_id"] assert data["checking_id"]
await update_payment_status(data["checking_id"], status=PaymentState.SUCCESS) payment = await get_payment(data["checking_id"])
payment.status = PaymentState.SUCCESS
await update_payment(payment)
params = {"time[ge]": ts, "time[le]": time()} params = {"time[ge]": ts, "time[le]": time()}
return fake_data, params return fake_data, params
+3 -2
View File
@@ -4,7 +4,7 @@ import hashlib
import pytest import pytest
from lnbits import bolt11 from lnbits import bolt11
from lnbits.core.crud import get_standalone_payment, update_payment_details from lnbits.core.crud import get_standalone_payment, update_payment
from lnbits.core.models import CreateInvoice, Payment, PaymentState from lnbits.core.models import CreateInvoice, Payment, PaymentState
from lnbits.core.services import fee_reserve_total, get_balance_delta from lnbits.core.services import fee_reserve_total, get_balance_delta
from lnbits.tasks import create_task, wait_for_paid_invoices from lnbits.tasks import create_task, wait_for_paid_invoices
@@ -303,7 +303,8 @@ async def test_receive_real_invoice_set_pending_and_check_state(
assert payment assert payment
# set the incoming invoice to pending # set the incoming invoice to pending
await update_payment_details(payment.checking_id, status=PaymentState.PENDING) payment.status = PaymentState.PENDING
await update_payment(payment)
payment_pending = await get_standalone_payment( payment_pending = await get_standalone_payment(
invoice["payment_hash"], incoming=True invoice["payment_hash"], incoming=True