[feat]: invoice amount settings (#2990)

This commit is contained in:
Vlad Stan 2025-02-24 14:45:17 +02:00 committed by GitHub
parent 10682cf736
commit 30cf6913af
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 128 additions and 46 deletions

View file

@ -63,6 +63,8 @@ jobs:
LNBITS_ENDPOINT: http://localhost:5001 LNBITS_ENDPOINT: http://localhost:5001
LNBITS_KEY: "d08a3313322a4514af75d488bcc27eee" LNBITS_KEY: "d08a3313322a4514af75d488bcc27eee"
ECLAIR_URL: http://127.0.0.1:8082 ECLAIR_URL: http://127.0.0.1:8082
LNBITS_MAX_OUTGOING_PAYMENT_AMOUNT_SATS: 1000000000
LNBITS_MAX_INCOMING_PAYMENT_AMOUNT_SATS: 1000000000
ECLAIR_PASS: lnbits ECLAIR_PASS: lnbits
PYTHONUNBUFFERED: 1 PYTHONUNBUFFERED: 1
DEBUG: true DEBUG: true

View file

@ -120,6 +120,12 @@ async def create_invoice(
amount, user_wallet, currency, extra amount, user_wallet, currency, extra
) )
if amount_sat > settings.lnbits_max_incoming_payment_amount_sats:
raise InvoiceError(
f"Invoice amount {amount_sat} sats is too high. Max allowed: "
f"{settings.lnbits_max_incoming_payment_amount_sats} sats.",
status="failed",
)
if settings.is_wallet_max_balance_exceeded( if settings.is_wallet_max_balance_exceeded(
user_wallet.balance_msat / 1000 + amount_sat user_wallet.balance_msat / 1000 + amount_sat
): ):
@ -707,8 +713,14 @@ def _validate_payment_request(
if not invoice.amount_msat or not invoice.amount_msat > 0: if not invoice.amount_msat or not invoice.amount_msat > 0:
raise PaymentError("Amountless invoices not supported.", status="failed") raise PaymentError("Amountless invoices not supported.", status="failed")
if max_sat and invoice.amount_msat > max_sat * 1000: max_sat = max_sat or settings.lnbits_max_outgoing_payment_amount_sats
raise PaymentError("Amount in invoice is too high.", status="failed") max_sat = min(max_sat, settings.lnbits_max_outgoing_payment_amount_sats)
if invoice.amount_msat > max_sat * 1000:
raise PaymentError(
f"Invoice amount {invoice.amount_msat // 1000} sats is too high. "
f"Max allowed: {max_sat} sats.",
status="failed",
)
return invoice return invoice

View file

@ -282,43 +282,6 @@
</div> </div>
</div> </div>
<div class="col-12 col-md-12">
<p v-text="$t('wallet_limiter')"></p>
<div class="row q-col-gutter-md">
<div class="col-3">
<q-input
filled
type="number"
v-model.number="formData.lnbits_wallet_limit_max_balance"
:label="$t('wallet_max_ballance')"
></q-input>
</div>
<div class="col-3">
<q-input
filled
type="number"
v-model.number="formData.lnbits_wallet_limit_daily_max_withdraw"
:label="$t('wallet_limit_max_withdraw_per_day')"
></q-input>
</div>
<div class="col-3">
<q-input
filled
type="number"
v-model.number="formData.lnbits_wallet_limit_secs_between_trans"
:label="$t('wallet_limit_secs_between_trans')"
></q-input>
</div>
<div class="col-3">
<q-toggle
v-model="formData.lnbits_only_allow_incoming_payments"
:label="$t('only_incoming_payments_allowed')"
><q-tooltip v-text="$t('disable_outgoing_payments')"></q-tooltip
></q-toggle>
</div>
</div>
</div>
<div class="col-12 col-md-12"> <div class="col-12 col-md-12">
<p v-text="$t('callback_url_rules')"></p> <p v-text="$t('callback_url_rules')"></p>
<div class="row q-col-gutter-md"> <div class="row q-col-gutter-md">

View file

@ -13,7 +13,6 @@
:label="$t('allowed_currencies')" :label="$t('allowed_currencies')"
:options="{{ currencies | safe }}" :options="{{ currencies | safe }}"
></q-select> ></q-select>
<br />
</div> </div>
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
<p><span v-text="$t('default_account_currency')"></span></p> <p><span v-text="$t('default_account_currency')"></span></p>
@ -25,11 +24,77 @@
:label="$t('currency')" :label="$t('currency')"
:options="formData.lnbits_allowed_currencies?.length ? formData.lnbits_allowed_currencies : {{ currencies }}" :options="formData.lnbits_allowed_currencies?.length ? formData.lnbits_allowed_currencies : {{ currencies }}"
></q-select> ></q-select>
<br />
</div> </div>
</div> </div>
<br /> <q-separator class="q-mb-lg q-mt-sm"></q-separator>
<h6 class="q-my-none"><span v-text="$t('payments')"></span></h6>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-3">
<p><span v-text="$t('max_outgoing_payment_amount')"></span></p>
<q-input
filled
type="number"
v-model.number="formData.lnbits_max_outgoing_payment_amount_sats"
:label="$t('max_outgoing_payment_amount')"
step="1"
min="0"
:hint="$t('max_outgoing_payment_amount_desc')"
></q-input>
</div>
<div class="col-12 col-md-3">
<p><span v-text="$t('max_incoming_payment_amount')"></span></p>
<q-input
filled
type="number"
v-model.number="formData.lnbits_max_incoming_payment_amount_sats"
:label="$t('max_incoming_payment_amount')"
step="1"
min="0"
:hint="$t('max_incoming_payment_amount_desc')"
></q-input>
</div>
<div class="col-12 col-md-6"></div>
</div>
<q-separator class="q-mb-lg q-mt-sm"></q-separator>
<h6 class="q-my-none"><span v-text="$t('wallet_limiter')"></span></h6>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-3">
<q-input
filled
type="number"
v-model.number="formData.lnbits_wallet_limit_max_balance"
:label="$t('wallet_max_ballance')"
></q-input>
</div>
<div class="col-12 col-md-3">
<q-input
filled
type="number"
v-model.number="formData.lnbits_wallet_limit_daily_max_withdraw"
:label="$t('wallet_limit_max_withdraw_per_day')"
></q-input>
</div>
<div class="col-12 col-md-3">
<q-input
filled
type="number"
v-model.number="formData.lnbits_wallet_limit_secs_between_trans"
:label="$t('wallet_limit_secs_between_trans')"
></q-input>
</div>
<div class="col-12 col-md-3">
<q-toggle
v-model="formData.lnbits_only_allow_incoming_payments"
:label="$t('only_incoming_payments_allowed')"
><q-tooltip v-text="$t('disable_outgoing_payments')"></q-tooltip
></q-toggle>
</div>
</div>
<q-separator class="q-mb-lg q-mt-sm"></q-separator>
<h6 class="q-my-none"><span v-text="$t('service_fee')"></span></h6> <h6 class="q-my-none"><span v-text="$t('service_fee')"></span></h6>
<div class="row q-col-gutter-md"> <div class="row q-col-gutter-md">
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">

View file

@ -378,6 +378,9 @@ class SecuritySettings(LNbitsSettings):
lnbits_watchdog_interval_minutes: int = Field(default=60) lnbits_watchdog_interval_minutes: int = Field(default=60)
lnbits_watchdog_delta: int = Field(default=1_000_000) lnbits_watchdog_delta: int = Field(default=1_000_000)
lnbits_max_outgoing_payment_amount_sats: int = Field(default=10_000_000)
lnbits_max_incoming_payment_amount_sats: int = Field(default=10_000_000)
def is_wallet_max_balance_exceeded(self, amount): def is_wallet_max_balance_exceeded(self, amount):
return ( return (
self.lnbits_wallet_limit_max_balance self.lnbits_wallet_limit_max_balance

File diff suppressed because one or more lines are too long

View file

@ -449,6 +449,13 @@ window.localisation.en = {
allowed_currencies_hint: 'Limit the number of available fiat currencies', allowed_currencies_hint: 'Limit the number of available fiat currencies',
default_account_currency: 'Default Account Currency', default_account_currency: 'Default Account Currency',
default_account_currency_hint: 'Default currency for accounting', default_account_currency_hint: 'Default currency for accounting',
max_incoming_payment_amount: 'Max Incoming Payment Amount',
max_incoming_payment_amount_desc:
'Maximum amount allowed for generating an invoice',
max_outgoing_payment_amount: 'Max Outgoing Payment Amount',
max_outgoing_payment_amount_desc:
'Maximum amount allowed for making a payment',
service_fee: 'Service Fee', service_fee: 'Service Fee',
service_fee_label: 'Service fee (%)', service_fee_label: 'Service fee (%)',
service_fee_hint: 'Fee charged per tx (%)', service_fee_hint: 'Fee charged per tx (%)',

View file

@ -310,3 +310,5 @@ def _settings_cleanup(settings: Settings):
settings.lnbits_wallet_limit_daily_max_withdraw = 0 settings.lnbits_wallet_limit_daily_max_withdraw = 0
settings.lnbits_admin_extensions = [] settings.lnbits_admin_extensions = []
settings.lnbits_admin_users = [] settings.lnbits_admin_users = []
settings.lnbits_max_outgoing_payment_amount_sats = 10_000_000_100
settings.lnbits_max_incoming_payment_amount_sats = 10_000_000_200

View file

@ -11,7 +11,7 @@ from pytest_mock.plugin import MockerFixture
from lnbits.core.crud import get_standalone_payment, get_wallet from lnbits.core.crud import get_standalone_payment, get_wallet
from lnbits.core.models import Payment, PaymentState, Wallet from lnbits.core.models import Payment, PaymentState, Wallet
from lnbits.core.services import create_invoice, pay_invoice from lnbits.core.services import create_invoice, pay_invoice
from lnbits.exceptions import PaymentError from lnbits.exceptions import InvoiceError, PaymentError
from lnbits.settings import Settings from lnbits.settings import Settings
from lnbits.tasks import ( from lnbits.tasks import (
create_permanent_task, create_permanent_task,
@ -61,9 +61,12 @@ async def test_bad_wallet_id(to_wallet: Wallet):
@pytest.mark.anyio @pytest.mark.anyio
async def test_payment_limit(to_wallet: Wallet): async def test_payment_explicit_limit(to_wallet: Wallet):
payment = await create_invoice(wallet_id=to_wallet.id, amount=101, memo="") payment = await create_invoice(wallet_id=to_wallet.id, amount=101, memo="")
with pytest.raises(PaymentError, match="Amount in invoice is too high."): with pytest.raises(
PaymentError,
match="Invoice amount 101 sats is too high. Max allowed: 100 sats.",
):
await pay_invoice( await pay_invoice(
wallet_id=to_wallet.id, wallet_id=to_wallet.id,
max_sat=100, max_sat=100,
@ -71,6 +74,31 @@ async def test_payment_limit(to_wallet: Wallet):
) )
@pytest.mark.anyio
async def test_payment_system_limit(to_wallet: Wallet, settings: Settings):
settings.lnbits_max_outgoing_payment_amount_sats = 100
payment = await create_invoice(wallet_id=to_wallet.id, amount=200, memo="")
with pytest.raises(
PaymentError,
match="Invoice amount 200 sats is too high. Max allowed: 100 sats.",
):
await pay_invoice(
wallet_id=to_wallet.id,
payment_request=payment.bolt11,
)
@pytest.mark.anyio
async def test_create_payment_system_limit(to_wallet: Wallet, settings: Settings):
settings.lnbits_max_incoming_payment_amount_sats = 101
with pytest.raises(
InvoiceError,
match="Invoice amount 202 sats is too high. Max allowed: 101 sats.",
):
await create_invoice(wallet_id=to_wallet.id, amount=202, memo="")
@pytest.mark.anyio @pytest.mark.anyio
async def test_pay_twice(to_wallet: Wallet): async def test_pay_twice(to_wallet: Wallet):
payment = await create_invoice(wallet_id=to_wallet.id, amount=3, memo="Twice") payment = await create_invoice(wallet_id=to_wallet.id, amount=3, memo="Twice")