Compare commits
2
Commits
trans_exts
...
v1.5.5-rc1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce57d08163 | ||
|
|
52304e0730 |
@@ -306,7 +306,7 @@ async def update_payment_checking_id(
|
|||||||
await (conn or db).execute(
|
await (conn or db).execute(
|
||||||
f"""
|
f"""
|
||||||
UPDATE apipayments
|
UPDATE apipayments
|
||||||
SET checking_id = :new_id, updated_at = {db.timestamp_placeholder('now')}
|
SET checking_id = :new_id, updated_at = {db.timestamp_placeholder("now")}
|
||||||
WHERE checking_id = :old_id
|
WHERE checking_id = :old_id
|
||||||
""", # noqa: S608
|
""", # noqa: S608
|
||||||
{
|
{
|
||||||
@@ -399,7 +399,6 @@ async def get_payment_count_stats(
|
|||||||
user_id: str | None = None,
|
user_id: str | None = None,
|
||||||
conn: Connection | None = None,
|
conn: Connection | None = None,
|
||||||
) -> list[PaymentCountStat]:
|
) -> list[PaymentCountStat]:
|
||||||
|
|
||||||
if not filters:
|
if not filters:
|
||||||
filters = Filters()
|
filters = Filters()
|
||||||
extra_stmts = []
|
extra_stmts = []
|
||||||
@@ -432,7 +431,6 @@ async def get_daily_stats(
|
|||||||
user_id: str | None = None,
|
user_id: str | None = None,
|
||||||
conn: Connection | None = None,
|
conn: Connection | None = None,
|
||||||
) -> tuple[list[PaymentDailyStats], list[PaymentDailyStats]]:
|
) -> tuple[list[PaymentDailyStats], list[PaymentDailyStats]]:
|
||||||
|
|
||||||
if not filters:
|
if not filters:
|
||||||
filters = Filters()
|
filters = Filters()
|
||||||
|
|
||||||
@@ -482,7 +480,6 @@ async def get_wallets_stats(
|
|||||||
user_id: str | None = None,
|
user_id: str | None = None,
|
||||||
conn: Connection | None = None,
|
conn: Connection | None = None,
|
||||||
) -> list[PaymentWalletStats]:
|
) -> list[PaymentWalletStats]:
|
||||||
|
|
||||||
if not filters:
|
if not filters:
|
||||||
filters = Filters()
|
filters = Filters()
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ from .payments import (
|
|||||||
PaymentState,
|
PaymentState,
|
||||||
PaymentWalletStats,
|
PaymentWalletStats,
|
||||||
SettleInvoice,
|
SettleInvoice,
|
||||||
|
UpdatePaymentExtra,
|
||||||
)
|
)
|
||||||
from .tinyurl import TinyURL
|
from .tinyurl import TinyURL
|
||||||
from .users import (
|
from .users import (
|
||||||
@@ -90,6 +91,7 @@ __all__ = [
|
|||||||
"SimpleStatus",
|
"SimpleStatus",
|
||||||
"TinyURL",
|
"TinyURL",
|
||||||
"UpdateBalance",
|
"UpdateBalance",
|
||||||
|
"UpdatePaymentExtra",
|
||||||
"UpdateSuperuserPassword",
|
"UpdateSuperuserPassword",
|
||||||
"UpdateUser",
|
"UpdateUser",
|
||||||
"UpdateUserPassword",
|
"UpdateUserPassword",
|
||||||
|
|||||||
@@ -35,6 +35,11 @@ class PaymentExtra(BaseModel):
|
|||||||
lnurl_response: str | None = None
|
lnurl_response: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class UpdatePaymentExtra(BaseModel):
|
||||||
|
payment_hash: str
|
||||||
|
extra: dict = Field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
class PayInvoice(BaseModel):
|
class PayInvoice(BaseModel):
|
||||||
payment_request: str
|
payment_request: str
|
||||||
description: str | None = None
|
description: str | None = None
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ from lnbits.core.models import (
|
|||||||
PaymentWalletStats,
|
PaymentWalletStats,
|
||||||
SettleInvoice,
|
SettleInvoice,
|
||||||
SimpleStatus,
|
SimpleStatus,
|
||||||
|
UpdatePaymentExtra,
|
||||||
)
|
)
|
||||||
from lnbits.core.models.payments import UpdatePaymentLabels
|
from lnbits.core.models.payments import UpdatePaymentLabels
|
||||||
from lnbits.core.models.users import AccountId
|
from lnbits.core.models.users import AccountId
|
||||||
@@ -297,6 +298,38 @@ async def api_update_payment_labels(
|
|||||||
return SimpleStatus(success=True, message="Payment labels updated.")
|
return SimpleStatus(success=True, message="Payment labels updated.")
|
||||||
|
|
||||||
|
|
||||||
|
@payment_router.patch(
|
||||||
|
"/extra",
|
||||||
|
name="Update payment extra",
|
||||||
|
description="Append new extra metadata to a payment.",
|
||||||
|
response_model=Payment,
|
||||||
|
)
|
||||||
|
async def api_update_payment_extra(
|
||||||
|
data: UpdatePaymentExtra,
|
||||||
|
key_type: WalletTypeInfo = Depends(require_admin_key),
|
||||||
|
) -> Payment:
|
||||||
|
payment = await get_standalone_payment(
|
||||||
|
data.payment_hash, wallet_id=key_type.wallet.id
|
||||||
|
)
|
||||||
|
if payment is None:
|
||||||
|
raise HTTPException(HTTPStatus.NOT_FOUND, "Payment does not exist.")
|
||||||
|
if not payment.success:
|
||||||
|
raise HTTPException(
|
||||||
|
HTTPStatus.BAD_REQUEST, "Payment extra can only be updated after success."
|
||||||
|
)
|
||||||
|
|
||||||
|
duplicate_keys = sorted(set(payment.extra).intersection(data.extra))
|
||||||
|
if duplicate_keys:
|
||||||
|
raise HTTPException(
|
||||||
|
HTTPStatus.BAD_REQUEST,
|
||||||
|
f"Extra keys already exist: {', '.join(duplicate_keys)}.",
|
||||||
|
)
|
||||||
|
|
||||||
|
payment.extra.update(data.extra)
|
||||||
|
await update_payment(payment)
|
||||||
|
return payment
|
||||||
|
|
||||||
|
|
||||||
@payment_router.get("/fee-reserve")
|
@payment_router.get("/fee-reserve")
|
||||||
async def api_payments_fee_reserve(invoice: str = Query("invoice")) -> JSONResponse:
|
async def api_payments_fee_reserve(invoice: str = Query("invoice")) -> JSONResponse:
|
||||||
invoice_obj = bolt11.decode(invoice)
|
invoice_obj = bolt11.decode(invoice)
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "lnbits"
|
name = "lnbits"
|
||||||
version = "1.5.4"
|
version = "1.5.5-rc1"
|
||||||
requires-python = ">=3.10,<3.13"
|
requires-python = ">=3.10,<3.13"
|
||||||
description = "LNbits, free and open-source Lightning wallet and accounts system."
|
description = "LNbits, free and open-source Lightning wallet and accounts system."
|
||||||
authors = [{ name = "Alan Bits", email = "alan@lnbits.com" }]
|
authors = [{ name = "Alan Bits", email = "alan@lnbits.com" }]
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import pytest
|
|||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
|
|
||||||
from lnbits.core.crud.payments import create_payment, get_payments
|
from lnbits.core.crud.payments import create_payment, get_payment, get_payments
|
||||||
from lnbits.core.models import Account, CreateInvoice, PaymentFilters, PaymentState
|
from lnbits.core.models import Account, CreateInvoice, PaymentFilters, PaymentState
|
||||||
from lnbits.core.models.payments import CancelInvoice, CreatePayment, SettleInvoice
|
from lnbits.core.models.payments import CancelInvoice, CreatePayment, SettleInvoice
|
||||||
from lnbits.core.models.users import AccountId
|
from lnbits.core.models.users import AccountId
|
||||||
@@ -218,6 +218,164 @@ async def test_payment_api_fee_reserve_and_hold_invoice_actions(mocker):
|
|||||||
cancel_mock.assert_awaited_once()
|
cancel_mock.assert_awaited_once()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_payment_extra_update_appends_new_keys(
|
||||||
|
client,
|
||||||
|
to_wallet,
|
||||||
|
adminkey_headers_to,
|
||||||
|
):
|
||||||
|
payment_hash = uuid4().hex
|
||||||
|
checking_id = await _create_payment(
|
||||||
|
to_wallet.id,
|
||||||
|
amount_msat=1_000,
|
||||||
|
payment_hash=payment_hash,
|
||||||
|
tag="splitpayments",
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await client.patch(
|
||||||
|
"/api/v1/payments/extra",
|
||||||
|
headers=adminkey_headers_to,
|
||||||
|
json={
|
||||||
|
"payment_hash": payment_hash,
|
||||||
|
"extra": {"child": "daughter", "compliance_note": "reviewed"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
extra = response.json()["extra"]
|
||||||
|
assert extra["tag"] == "splitpayments"
|
||||||
|
assert extra["child"] == "daughter"
|
||||||
|
assert extra["compliance_note"] == "reviewed"
|
||||||
|
|
||||||
|
payment = await get_payment(checking_id)
|
||||||
|
assert payment.extra == extra
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_payment_extra_update_creates_extra_when_missing(
|
||||||
|
client,
|
||||||
|
to_wallet,
|
||||||
|
adminkey_headers_to,
|
||||||
|
):
|
||||||
|
payment_hash = uuid4().hex
|
||||||
|
checking_id = await _create_payment(
|
||||||
|
to_wallet.id,
|
||||||
|
amount_msat=1_000,
|
||||||
|
payment_hash=payment_hash,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await client.patch(
|
||||||
|
"/api/v1/payments/extra",
|
||||||
|
headers=adminkey_headers_to,
|
||||||
|
json={"payment_hash": payment_hash, "extra": {"note": "reviewed"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["extra"] == {"note": "reviewed"}
|
||||||
|
|
||||||
|
payment = await get_payment(checking_id)
|
||||||
|
assert payment.extra == {"note": "reviewed"}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_payment_extra_update_rejects_existing_keys(
|
||||||
|
client,
|
||||||
|
to_wallet,
|
||||||
|
adminkey_headers_to,
|
||||||
|
):
|
||||||
|
payment_hash = uuid4().hex
|
||||||
|
checking_id = await _create_payment(
|
||||||
|
to_wallet.id,
|
||||||
|
amount_msat=1_000,
|
||||||
|
payment_hash=payment_hash,
|
||||||
|
tag="original",
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await client.patch(
|
||||||
|
"/api/v1/payments/extra",
|
||||||
|
headers=adminkey_headers_to,
|
||||||
|
json={"payment_hash": payment_hash, "extra": {"tag": "overwritten"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert response.json()["detail"] == "Extra keys already exist: tag."
|
||||||
|
|
||||||
|
payment = await get_payment(checking_id)
|
||||||
|
assert payment.extra == {"tag": "original"}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_payment_extra_update_requires_admin_key(
|
||||||
|
client,
|
||||||
|
to_wallet,
|
||||||
|
inkey_headers_to,
|
||||||
|
):
|
||||||
|
payment_hash = uuid4().hex
|
||||||
|
await _create_payment(
|
||||||
|
to_wallet.id,
|
||||||
|
amount_msat=1_000,
|
||||||
|
payment_hash=payment_hash,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await client.patch(
|
||||||
|
"/api/v1/payments/extra",
|
||||||
|
headers=inkey_headers_to,
|
||||||
|
json={"payment_hash": payment_hash, "extra": {"note": "invoice key"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 403
|
||||||
|
assert response.json()["detail"] == "Invalid adminkey."
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_payment_extra_update_is_wallet_scoped(
|
||||||
|
client,
|
||||||
|
from_wallet,
|
||||||
|
adminkey_headers_to,
|
||||||
|
):
|
||||||
|
payment_hash = uuid4().hex
|
||||||
|
await _create_payment(
|
||||||
|
from_wallet.id,
|
||||||
|
amount_msat=1_000,
|
||||||
|
payment_hash=payment_hash,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await client.patch(
|
||||||
|
"/api/v1/payments/extra",
|
||||||
|
headers=adminkey_headers_to,
|
||||||
|
json={"payment_hash": payment_hash, "extra": {"note": "wrong wallet"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 404
|
||||||
|
assert response.json()["detail"] == "Payment does not exist."
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_payment_extra_update_requires_successful_payment(
|
||||||
|
client,
|
||||||
|
to_wallet,
|
||||||
|
adminkey_headers_to,
|
||||||
|
):
|
||||||
|
payment_hash = uuid4().hex
|
||||||
|
await _create_payment(
|
||||||
|
to_wallet.id,
|
||||||
|
amount_msat=1_000,
|
||||||
|
payment_hash=payment_hash,
|
||||||
|
status=PaymentState.PENDING,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await client.patch(
|
||||||
|
"/api/v1/payments/extra",
|
||||||
|
headers=adminkey_headers_to,
|
||||||
|
json={"payment_hash": payment_hash, "extra": {"note": "too early"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert (
|
||||||
|
response.json()["detail"] == "Payment extra can only be updated after success."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def _create_payment(
|
async def _create_payment(
|
||||||
wallet_id: str,
|
wallet_id: str,
|
||||||
*,
|
*,
|
||||||
|
|||||||
Reference in New Issue
Block a user