mirror of
https://github.com/lnbits/lnbits.git
synced 2025-07-08 14:30:43 +02:00
Wallet limits: max balance, daily max withdraw, transactions per sec (#2223)
Co-authored-by: benarc <ben@arc.wales> Co-authored-by: Pavol Rusnak <pavol@rusnak.io> Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
This commit is contained in:
@ -200,6 +200,13 @@ LNBITS_RESERVE_FEE_MIN=2000
|
|||||||
# value in percent
|
# value in percent
|
||||||
LNBITS_RESERVE_FEE_PERCENT=1.0
|
LNBITS_RESERVE_FEE_PERCENT=1.0
|
||||||
|
|
||||||
|
# limit the maximum balance for each wallet
|
||||||
|
# throw an error if the wallet attempts to create a new invoice
|
||||||
|
|
||||||
|
# LNBITS_WALLET_LIMIT_MAX_BALANCE=1000000
|
||||||
|
# LNBITS_WALLET_LIMIT_DAILY_MAX_WITHDRAW=1000000
|
||||||
|
# LNBITS_WALLET_LIMIT_SECS_BETWEEN_TRANS=60
|
||||||
|
|
||||||
# Limit fiat currencies allowed to see in UI
|
# Limit fiat currencies allowed to see in UI
|
||||||
# LNBITS_ALLOWED_CURRENCIES="EUR, USD"
|
# LNBITS_ALLOWED_CURRENCIES="EUR, USD"
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import datetime
|
import datetime
|
||||||
import json
|
import json
|
||||||
|
import time
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, List, Optional, Tuple, TypedDict
|
from typing import Dict, List, Optional, Tuple, TypedDict
|
||||||
@ -41,6 +42,7 @@ from .crud import (
|
|||||||
create_wallet,
|
create_wallet,
|
||||||
delete_wallet_payment,
|
delete_wallet_payment,
|
||||||
get_account,
|
get_account,
|
||||||
|
get_payments,
|
||||||
get_standalone_payment,
|
get_standalone_payment,
|
||||||
get_super_settings,
|
get_super_settings,
|
||||||
get_total_balance,
|
get_total_balance,
|
||||||
@ -118,8 +120,9 @@ async def create_invoice(
|
|||||||
if not amount > 0:
|
if not amount > 0:
|
||||||
raise InvoiceFailure("Amountless invoices not supported.")
|
raise InvoiceFailure("Amountless invoices not supported.")
|
||||||
|
|
||||||
if await get_wallet(wallet_id, conn=conn) is None:
|
user_wallet = await get_wallet(wallet_id, conn=conn)
|
||||||
raise InvoiceFailure("Wallet does not exist.")
|
if not user_wallet:
|
||||||
|
raise InvoiceFailure(f"Could not fetch wallet '{wallet_id}'.")
|
||||||
|
|
||||||
invoice_memo = None if description_hash else memo
|
invoice_memo = None if description_hash else memo
|
||||||
|
|
||||||
@ -130,6 +133,14 @@ async def create_invoice(
|
|||||||
amount, wallet_id, currency=currency, extra=extra, conn=conn
|
amount, wallet_id, currency=currency, extra=extra, conn=conn
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if settings.is_wallet_max_balance_exceeded(
|
||||||
|
user_wallet.balance_msat / 1000 + amount_sat
|
||||||
|
):
|
||||||
|
raise InvoiceFailure(
|
||||||
|
f"Wallet balance cannot exceed "
|
||||||
|
f"{settings.lnbits_wallet_limit_max_balance} sats."
|
||||||
|
)
|
||||||
|
|
||||||
ok, checking_id, payment_request, error_message = await wallet.create_invoice(
|
ok, checking_id, payment_request, error_message = await wallet.create_invoice(
|
||||||
amount=amount_sat,
|
amount=amount_sat,
|
||||||
memo=invoice_memo,
|
memo=invoice_memo,
|
||||||
@ -185,6 +196,8 @@ async def pay_invoice(
|
|||||||
if max_sat and invoice.amount_msat > max_sat * 1000:
|
if max_sat and invoice.amount_msat > max_sat * 1000:
|
||||||
raise ValueError("Amount in invoice is too high.")
|
raise ValueError("Amount in invoice is too high.")
|
||||||
|
|
||||||
|
await check_wallet_limits(wallet_id, conn, 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}"
|
||||||
@ -372,6 +385,58 @@ async def pay_invoice(
|
|||||||
return invoice.payment_hash
|
return invoice.payment_hash
|
||||||
|
|
||||||
|
|
||||||
|
async def check_wallet_limits(wallet_id, conn, amount_msat):
|
||||||
|
await check_time_limit_between_transactions(conn, wallet_id)
|
||||||
|
await check_wallet_daily_withdraw_limit(conn, wallet_id, amount_msat)
|
||||||
|
|
||||||
|
|
||||||
|
async def check_time_limit_between_transactions(conn, wallet_id):
|
||||||
|
limit = settings.lnbits_wallet_limit_secs_between_trans
|
||||||
|
if not limit or limit <= 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
payments = await get_payments(
|
||||||
|
since=int(time.time()) - limit,
|
||||||
|
wallet_id=wallet_id,
|
||||||
|
limit=1,
|
||||||
|
conn=conn,
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(payments) == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
raise ValueError(
|
||||||
|
f"The time limit of {limit} seconds between payments has been reached."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def check_wallet_daily_withdraw_limit(conn, wallet_id, amount_msat):
|
||||||
|
limit = settings.lnbits_wallet_limit_daily_max_withdraw
|
||||||
|
if not limit or limit <= 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
payments = await get_payments(
|
||||||
|
since=int(time.time()) - 60 * 60 * 24,
|
||||||
|
outgoing=True,
|
||||||
|
wallet_id=wallet_id,
|
||||||
|
limit=1,
|
||||||
|
conn=conn,
|
||||||
|
)
|
||||||
|
if len(payments) == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
total = 0
|
||||||
|
for pay in payments:
|
||||||
|
total += pay.amount
|
||||||
|
total = total - amount_msat
|
||||||
|
if limit * 1000 + total < 0:
|
||||||
|
raise ValueError(
|
||||||
|
"Daily withdrawal limit of "
|
||||||
|
+ str(settings.lnbits_wallet_limit_daily_max_withdraw)
|
||||||
|
+ " sats reached."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def redeem_lnurl_withdraw(
|
async def redeem_lnurl_withdraw(
|
||||||
wallet_id: str,
|
wallet_id: str,
|
||||||
lnurl_request: str,
|
lnurl_request: str,
|
||||||
|
@ -180,6 +180,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12 col-md-12">
|
<div class="col-12 col-md-12">
|
||||||
<p v-text="$t('rate_limiter')"></p>
|
<p v-text="$t('rate_limiter')"></p>
|
||||||
<div class="row q-col-gutter-md">
|
<div class="row q-col-gutter-md">
|
||||||
@ -201,6 +202,36 @@
|
|||||||
</div>
|
</div>
|
||||||
</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-4">
|
||||||
|
<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-4">
|
||||||
|
<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-4">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
|
@ -116,6 +116,9 @@ class SecuritySettings(LNbitsSettings):
|
|||||||
lnbits_notifications: bool = Field(default=False)
|
lnbits_notifications: bool = Field(default=False)
|
||||||
lnbits_killswitch: bool = Field(default=False)
|
lnbits_killswitch: bool = Field(default=False)
|
||||||
lnbits_killswitch_interval: int = Field(default=60)
|
lnbits_killswitch_interval: int = Field(default=60)
|
||||||
|
lnbits_wallet_limit_max_balance: int = Field(default=0)
|
||||||
|
lnbits_wallet_limit_daily_max_withdraw: int = Field(default=0)
|
||||||
|
lnbits_wallet_limit_secs_between_trans: int = Field(default=0)
|
||||||
lnbits_watchdog: bool = Field(default=False)
|
lnbits_watchdog: bool = Field(default=False)
|
||||||
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)
|
||||||
@ -125,6 +128,13 @@ class SecuritySettings(LNbitsSettings):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def is_wallet_max_balance_exceeded(self, amount):
|
||||||
|
return (
|
||||||
|
self.lnbits_wallet_limit_max_balance
|
||||||
|
and self.lnbits_wallet_limit_max_balance > 0
|
||||||
|
and amount > self.lnbits_wallet_limit_max_balance
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class FakeWalletFundingSource(LNbitsSettings):
|
class FakeWalletFundingSource(LNbitsSettings):
|
||||||
fake_wallet_secret: str = Field(default="ToTheMoon1")
|
fake_wallet_secret: str = Field(default="ToTheMoon1")
|
||||||
|
2
lnbits/static/bundle.min.js
vendored
2
lnbits/static/bundle.min.js
vendored
File diff suppressed because one or more lines are too long
@ -185,6 +185,12 @@ window.localisation.en = {
|
|||||||
allow_access_hint: 'Allow access by IP (will override blocked IPs)',
|
allow_access_hint: 'Allow access by IP (will override blocked IPs)',
|
||||||
enter_ip: 'Enter IP and hit enter',
|
enter_ip: 'Enter IP and hit enter',
|
||||||
rate_limiter: 'Rate Limiter',
|
rate_limiter: 'Rate Limiter',
|
||||||
|
wallet_limiter: 'Wallet Limiter',
|
||||||
|
wallet_limit_max_withdraw_per_day:
|
||||||
|
'Max daily wallet withdrawal in sats (0 to disable)',
|
||||||
|
wallet_max_ballance: 'Wallet max balance in sats (0 to disable)',
|
||||||
|
wallet_limit_secs_between_trans:
|
||||||
|
'Min secs between transactions per wallet (0 to disable)',
|
||||||
number_of_requests: 'Number of requests',
|
number_of_requests: 'Number of requests',
|
||||||
time_unit: 'Time unit',
|
time_unit: 'Time unit',
|
||||||
minute: 'minute',
|
minute: 'minute',
|
||||||
|
Reference in New Issue
Block a user