mirror of
https://github.com/lnbits/lnbits-legend.git
synced 2025-01-18 21:32:38 +01:00
Add service fee to specific wallet (#2050)
* add service fee to specific wallet
* add to .env.example
* Added service fee wallet to manage server
* cleaned
* prettier
* Added badge for service fee
* Added tooltip
* Added service fee max
* allow ignoring service fee for internal transactions
* add fee_reserve_total helper funciton that includes service_fee
* html for admin ui
* typo
* Update .env.example
Co-authored-by: Pavol Rusnak <pavol@rusnak.io>
* fix .env.template comment
* bundle
* WIP: expose fee reserve endpoint
---------
Co-authored-by: Arc <ben@arc.wales>
Co-authored-by: dni ⚡ <office@dnilabs.com>
Co-authored-by: Pavol Rusnak <pavol@rusnak.io>
This commit is contained in:
parent
4dcf26bcb3
commit
6a27b91fcb
@ -78,7 +78,13 @@ LNBITS_EXTENSIONS_DEFAULT_INSTALL="tpos"
|
|||||||
LNBITS_DATA_FOLDER="./data"
|
LNBITS_DATA_FOLDER="./data"
|
||||||
# LNBITS_DATABASE_URL="postgres://user:password@host:port/databasename"
|
# LNBITS_DATABASE_URL="postgres://user:password@host:port/databasename"
|
||||||
|
|
||||||
LNBITS_SERVICE_FEE="0.0"
|
# Value in percent
|
||||||
|
LNBITS_SERVICE_FEE=0.0
|
||||||
|
# Max fee to charge per transaction (in satoshi), 0 will leave no max and use LNBITS_SERVICE_FEE %
|
||||||
|
LNBITS_SERVICE_FEE_MAX=0
|
||||||
|
# The wallet where the service fee will be sent to
|
||||||
|
LNBITS_SERVICE_FEE_WALLET=""
|
||||||
|
|
||||||
# value in millisats
|
# value in millisats
|
||||||
LNBITS_RESERVE_FEE_MIN=2000
|
LNBITS_RESERVE_FEE_MIN=2000
|
||||||
# value in percent
|
# value in percent
|
||||||
|
@ -426,6 +426,8 @@ def log_server_info():
|
|||||||
logger.info(f"Data folder: {settings.lnbits_data_folder}")
|
logger.info(f"Data folder: {settings.lnbits_data_folder}")
|
||||||
logger.info(f"Database: {get_db_vendor_name()}")
|
logger.info(f"Database: {get_db_vendor_name()}")
|
||||||
logger.info(f"Service fee: {settings.lnbits_service_fee}")
|
logger.info(f"Service fee: {settings.lnbits_service_fee}")
|
||||||
|
logger.info(f"Service fee max: {settings.lnbits_service_fee_max}")
|
||||||
|
logger.info(f"Service fee wallet: {settings.lnbits_service_fee_wallet}")
|
||||||
|
|
||||||
|
|
||||||
def get_db_vendor_name():
|
def get_db_vendor_name():
|
||||||
|
@ -185,7 +185,6 @@ 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.")
|
||||||
|
|
||||||
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}"
|
||||||
@ -228,6 +227,9 @@ async def pay_invoice(
|
|||||||
# (pending only)
|
# (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:
|
||||||
|
fee_reserve_total_msat = fee_reserve_total(
|
||||||
|
invoice.amount_msat, internal=True
|
||||||
|
)
|
||||||
# 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(
|
||||||
@ -244,19 +246,22 @@ async def pay_invoice(
|
|||||||
# create a new payment from this wallet
|
# create a new payment from this wallet
|
||||||
new_payment = await create_payment(
|
new_payment = await create_payment(
|
||||||
checking_id=internal_id,
|
checking_id=internal_id,
|
||||||
fee=0,
|
fee=0 + abs(fee_reserve_total_msat),
|
||||||
pending=False,
|
pending=False,
|
||||||
conn=conn,
|
conn=conn,
|
||||||
**payment_kwargs,
|
**payment_kwargs,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
fee_reserve_total_msat = fee_reserve_total(
|
||||||
|
invoice.amount_msat, internal=False
|
||||||
|
)
|
||||||
logger.debug(f"creating temporary payment with id {temp_id}")
|
logger.debug(f"creating temporary payment with id {temp_id}")
|
||||||
# create a temporary payment here so we can check if
|
# create a temporary payment here so we can check if
|
||||||
# the balance is enough in the next step
|
# the balance is enough in the next step
|
||||||
try:
|
try:
|
||||||
new_payment = await create_payment(
|
new_payment = await create_payment(
|
||||||
checking_id=temp_id,
|
checking_id=temp_id,
|
||||||
fee=-fee_reserve_msat,
|
fee=-abs(fee_reserve_total_msat),
|
||||||
conn=conn,
|
conn=conn,
|
||||||
**payment_kwargs,
|
**payment_kwargs,
|
||||||
)
|
)
|
||||||
@ -270,14 +275,18 @@ async def pay_invoice(
|
|||||||
assert wallet, "Wallet for balancecheck could not be fetched"
|
assert wallet, "Wallet for balancecheck could not be fetched"
|
||||||
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_msat:
|
if (
|
||||||
|
not internal_checking_id
|
||||||
|
and wallet.balance_msat > -fee_reserve_total_msat
|
||||||
|
):
|
||||||
raise PaymentFailure(
|
raise PaymentFailure(
|
||||||
f"You must reserve at least ({round(fee_reserve_msat/1000)} sat) to"
|
f"You must reserve at least ({round(fee_reserve_total_msat/1000)}"
|
||||||
" cover potential routing fees."
|
" sat) to cover potential routing fees."
|
||||||
)
|
)
|
||||||
raise PermissionError("Insufficient balance.")
|
raise PermissionError("Insufficient balance.")
|
||||||
|
|
||||||
if internal_checking_id:
|
if internal_checking_id:
|
||||||
|
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_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
|
||||||
@ -294,6 +303,8 @@ async def pay_invoice(
|
|||||||
logger.debug(f"enqueuing internal invoice {internal_checking_id}")
|
logger.debug(f"enqueuing internal invoice {internal_checking_id}")
|
||||||
await internal_invoice_queue.put(internal_checking_id)
|
await internal_invoice_queue.put(internal_checking_id)
|
||||||
else:
|
else:
|
||||||
|
fee_reserve_msat = fee_reserve(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
|
||||||
WALLET = get_wallet_class()
|
WALLET = get_wallet_class()
|
||||||
@ -315,7 +326,10 @@ async def pay_invoice(
|
|||||||
await update_payment_details(
|
await update_payment_details(
|
||||||
checking_id=temp_id,
|
checking_id=temp_id,
|
||||||
pending=payment.ok is not True,
|
pending=payment.ok is not True,
|
||||||
fee=payment.fee_msat,
|
fee=-(
|
||||||
|
abs(payment.fee_msat if payment.fee_msat else 0)
|
||||||
|
+ abs(service_fee_msat)
|
||||||
|
),
|
||||||
preimage=payment.preimage,
|
preimage=payment.preimage,
|
||||||
new_checking_id=payment.checking_id,
|
new_checking_id=payment.checking_id,
|
||||||
conn=conn,
|
conn=conn,
|
||||||
@ -343,6 +357,18 @@ async def pay_invoice(
|
|||||||
f" database: {temp_id}"
|
f" database: {temp_id}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# credit service fee wallet
|
||||||
|
if settings.lnbits_service_fee_wallet and service_fee_msat:
|
||||||
|
new_payment = await create_payment(
|
||||||
|
wallet_id=settings.lnbits_service_fee_wallet,
|
||||||
|
fee=0,
|
||||||
|
amount=abs(service_fee_msat),
|
||||||
|
memo="service fee",
|
||||||
|
checking_id="service_fee" + temp_id,
|
||||||
|
payment_request=payment_request,
|
||||||
|
payment_hash=invoice.payment_hash,
|
||||||
|
pending=False,
|
||||||
|
)
|
||||||
return invoice.payment_hash
|
return invoice.payment_hash
|
||||||
|
|
||||||
|
|
||||||
@ -496,12 +522,31 @@ async def check_transaction_status(
|
|||||||
|
|
||||||
# WARN: this same value must be used for balance check and passed to
|
# 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
|
# 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, internal: bool = False) -> 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
|
||||||
return max(int(reserve_min), int(amount_msat * reserve_percent / 100.0))
|
return max(int(reserve_min), int(amount_msat * reserve_percent / 100.0))
|
||||||
|
|
||||||
|
|
||||||
|
def service_fee(amount_msat: int, internal: bool = False) -> int:
|
||||||
|
service_fee_percent = settings.lnbits_service_fee
|
||||||
|
fee_max = settings.lnbits_service_fee_max * 1000
|
||||||
|
if settings.lnbits_service_fee_wallet:
|
||||||
|
if internal and settings.lnbits_service_fee_ignore_internal:
|
||||||
|
return 0
|
||||||
|
fee_percentage = int(amount_msat / 100 * service_fee_percent)
|
||||||
|
if fee_max > 0 and fee_percentage > fee_max:
|
||||||
|
return fee_max
|
||||||
|
else:
|
||||||
|
return fee_percentage
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def fee_reserve_total(amount_msat: int, internal: bool = False) -> int:
|
||||||
|
return fee_reserve(amount_msat, internal) + service_fee(amount_msat, internal)
|
||||||
|
|
||||||
|
|
||||||
async def send_payment_notification(wallet: Wallet, payment: Payment):
|
async def send_payment_notification(wallet: Wallet, payment: Payment):
|
||||||
await websocketUpdater(
|
await websocketUpdater(
|
||||||
wallet.id,
|
wallet.id,
|
||||||
|
@ -85,6 +85,64 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<h6 class="q-mt-xl q-mb-md">Service Fees</h6>
|
||||||
|
<div class="row q-col-gutter-md">
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<p>Service Fee</p>
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
type="number"
|
||||||
|
v-model.number="formData.lnbits_service_fee"
|
||||||
|
label="Service fee (%)"
|
||||||
|
step="0.1"
|
||||||
|
hint="Fee charged per tx (%)"
|
||||||
|
></q-input>
|
||||||
|
<br />
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<p>Service fee max</p>
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
type="number"
|
||||||
|
v-model.number="formData.lnbits_service_fee_max"
|
||||||
|
label="Service fee max (sats)"
|
||||||
|
hint="Max service fee to charge in (sats)"
|
||||||
|
></q-input>
|
||||||
|
<br />
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<p>Fee Wallet</p>
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
v-model="formData.lnbits_service_fee_wallet"
|
||||||
|
label="Fee wallet (wallet ID)"
|
||||||
|
hint="Wallet ID to send funds to"
|
||||||
|
></q-input>
|
||||||
|
<br />
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<p>Disable Service Fee for Internal Payments</p>
|
||||||
|
<q-item tag="label" v-ripple>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>Disable Fee</q-item-label>
|
||||||
|
<q-item-label caption
|
||||||
|
>Disable Service Fee for Internal Lightning
|
||||||
|
Payments</q-item-label
|
||||||
|
>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-toggle
|
||||||
|
size="md"
|
||||||
|
v-model="formData.lnbits_service_fee_ignore_internal"
|
||||||
|
checked-icon="check"
|
||||||
|
color="green"
|
||||||
|
unchecked-icon="clear"
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<br />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div v-if="isSuperUser">
|
<div v-if="isSuperUser">
|
||||||
<lnbits-funding-sources
|
<lnbits-funding-sources
|
||||||
:form-data="formData"
|
:form-data="formData"
|
||||||
|
@ -19,41 +19,6 @@
|
|||||||
<br />
|
<br />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row q-col-gutter-md">
|
|
||||||
<div class="col-12 col-md-6">
|
|
||||||
<p>Service Fee</p>
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
type="number"
|
|
||||||
v-model.number="formData.lnbits_service_fee"
|
|
||||||
label="Service fee (%)"
|
|
||||||
step="0.1"
|
|
||||||
hint="Fee charged per tx (%)"
|
|
||||||
></q-input>
|
|
||||||
<br />
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-md-6">
|
|
||||||
<p>Miscellaneous</p>
|
|
||||||
<q-item tag="label" v-ripple>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label>Hide API</q-item-label>
|
|
||||||
<q-item-label caption
|
|
||||||
>Hides wallet api, extensions can choose to honor</q-item-label
|
|
||||||
>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-toggle
|
|
||||||
size="md"
|
|
||||||
v-model="formData.lnbits_hide_api"
|
|
||||||
checked-icon="check"
|
|
||||||
color="green"
|
|
||||||
unchecked-icon="clear"
|
|
||||||
/>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
<br />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<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">
|
||||||
<p>Allowed currencies</p>
|
<p>Allowed currencies</p>
|
||||||
@ -93,6 +58,27 @@
|
|||||||
></q-select>
|
></q-select>
|
||||||
<br />
|
<br />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<p>Miscellaneous</p>
|
||||||
|
<q-item tag="label" v-ripple>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>Hide API</q-item-label>
|
||||||
|
<q-item-label caption
|
||||||
|
>Hides wallet api, extensions can choose to honor</q-item-label
|
||||||
|
>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-toggle
|
||||||
|
size="md"
|
||||||
|
v-model="formData.lnbits_hide_api"
|
||||||
|
checked-icon="check"
|
||||||
|
color="green"
|
||||||
|
unchecked-icon="clear"
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<br />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
@ -104,6 +104,7 @@ from ..services import (
|
|||||||
PaymentFailure,
|
PaymentFailure,
|
||||||
check_transaction_status,
|
check_transaction_status,
|
||||||
create_invoice,
|
create_invoice,
|
||||||
|
fee_reserve_total,
|
||||||
pay_invoice,
|
pay_invoice,
|
||||||
perform_lnurlauth,
|
perform_lnurlauth,
|
||||||
websocketManager,
|
websocketManager,
|
||||||
@ -386,6 +387,21 @@ async def api_payments_create(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@api_router.get("/api/v1/payments/fee-reserve")
|
||||||
|
async def api_payments_fee_reserve(invoice: str = Query("invoice")) -> JSONResponse:
|
||||||
|
invoice_obj = bolt11.decode(invoice)
|
||||||
|
if invoice_obj.amount_msat:
|
||||||
|
response = {
|
||||||
|
"fee_reserve": fee_reserve_total(invoice_obj.amount_msat),
|
||||||
|
}
|
||||||
|
return JSONResponse(response)
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
|
detail="Invoice has no amount.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@api_router.post("/api/v1/payments/lnurl")
|
@api_router.post("/api/v1/payments/lnurl")
|
||||||
async def api_payments_pay_lnurl(
|
async def api_payments_pay_lnurl(
|
||||||
data: CreateLnurl, wallet: WalletTypeInfo = Depends(require_admin_key)
|
data: CreateLnurl, wallet: WalletTypeInfo = Depends(require_admin_key)
|
||||||
|
@ -214,6 +214,7 @@ async def wallet(
|
|||||||
"user": user.dict(),
|
"user": user.dict(),
|
||||||
"wallet": userwallet.dict(),
|
"wallet": userwallet.dict(),
|
||||||
"service_fee": settings.lnbits_service_fee,
|
"service_fee": settings.lnbits_service_fee,
|
||||||
|
"service_fee_max": settings.lnbits_service_fee_max,
|
||||||
"web_manifest": f"/manifest/{user.id}.webmanifest",
|
"web_manifest": f"/manifest/{user.id}.webmanifest",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -59,6 +59,9 @@ def template_renderer(additional_folders: Optional[List] = None) -> Jinja2Templa
|
|||||||
t.env.globals["LNBITS_VERSION"] = settings.version
|
t.env.globals["LNBITS_VERSION"] = settings.version
|
||||||
t.env.globals["LNBITS_NEW_ACCOUNTS_ALLOWED"] = settings.new_accounts_allowed
|
t.env.globals["LNBITS_NEW_ACCOUNTS_ALLOWED"] = settings.new_accounts_allowed
|
||||||
t.env.globals["LNBITS_ADMIN_UI"] = settings.lnbits_admin_ui
|
t.env.globals["LNBITS_ADMIN_UI"] = settings.lnbits_admin_ui
|
||||||
|
t.env.globals["LNBITS_SERVICE_FEE"] = settings.lnbits_service_fee
|
||||||
|
t.env.globals["LNBITS_SERVICE_FEE_MAX"] = settings.lnbits_service_fee_max
|
||||||
|
t.env.globals["LNBITS_SERVICE_FEE_WALLET"] = settings.lnbits_service_fee_wallet
|
||||||
t.env.globals["LNBITS_NODE_UI"] = (
|
t.env.globals["LNBITS_NODE_UI"] = (
|
||||||
settings.lnbits_node_ui and get_node_class() is not None
|
settings.lnbits_node_ui and get_node_class() is not None
|
||||||
)
|
)
|
||||||
|
@ -99,6 +99,9 @@ class OpsSettings(LNbitsSettings):
|
|||||||
lnbits_reserve_fee_min: int = Field(default=2000)
|
lnbits_reserve_fee_min: int = Field(default=2000)
|
||||||
lnbits_reserve_fee_percent: float = Field(default=1.0)
|
lnbits_reserve_fee_percent: float = Field(default=1.0)
|
||||||
lnbits_service_fee: float = Field(default=0)
|
lnbits_service_fee: float = Field(default=0)
|
||||||
|
lnbits_service_fee_ignore_internal: bool = Field(default=True)
|
||||||
|
lnbits_service_fee_max: int = Field(default=0)
|
||||||
|
lnbits_service_fee_wallet: str = Field(default=None)
|
||||||
lnbits_hide_api: bool = Field(default=False)
|
lnbits_hide_api: bool = Field(default=False)
|
||||||
lnbits_denomination: str = Field(default="sats")
|
lnbits_denomination: str = Field(default="sats")
|
||||||
|
|
||||||
|
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
@ -54,6 +54,10 @@ window.localisation.en = {
|
|||||||
view_github: 'View on GitHub',
|
view_github: 'View on GitHub',
|
||||||
voidwallet_active: 'VoidWallet is active! Payments disabled',
|
voidwallet_active: 'VoidWallet is active! Payments disabled',
|
||||||
use_with_caution: 'USE WITH CAUTION - %{name} wallet is still in BETA',
|
use_with_caution: 'USE WITH CAUTION - %{name} wallet is still in BETA',
|
||||||
|
service_fee: 'Service fee: %{amount} % per transaction',
|
||||||
|
service_fee_max: 'Service fee: %{amount} % per transaction (max %{max} sats)',
|
||||||
|
service_fee_tooltip:
|
||||||
|
'Service fee charged by the LNbits server admin per outgoing transaction',
|
||||||
toggle_darkmode: 'Toggle Dark Mode',
|
toggle_darkmode: 'Toggle Dark Mode',
|
||||||
view_swagger_docs: 'View LNbits Swagger API docs',
|
view_swagger_docs: 'View LNbits Swagger API docs',
|
||||||
api_docs: 'API docs',
|
api_docs: 'API docs',
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
// update cache version every time there is a new deployment
|
// update cache version every time there is a new deployment
|
||||||
// so the service worker reinitializes the cache
|
// so the service worker reinitializes the cache
|
||||||
const CACHE_VERSION = 72
|
const CACHE_VERSION = 82
|
||||||
const CURRENT_CACHE = `lnbits-${CACHE_VERSION}-`
|
const CURRENT_CACHE = `lnbits-${CACHE_VERSION}-`
|
||||||
|
|
||||||
const getApiKey = request => {
|
const getApiKey = request => {
|
||||||
|
@ -74,7 +74,29 @@
|
|||||||
v-text='$t("use_with_caution", { name: "{{ SITE_TITLE }}" })'
|
v-text='$t("use_with_caution", { name: "{{ SITE_TITLE }}" })'
|
||||||
></span>
|
></span>
|
||||||
</q-badge>
|
</q-badge>
|
||||||
{% endblock %}
|
{% if LNBITS_SERVICE_FEE > 0 %}
|
||||||
|
<q-badge
|
||||||
|
v-show="$q.screen.gt.sm"
|
||||||
|
v-if="g.user"
|
||||||
|
color="green"
|
||||||
|
text-color="black"
|
||||||
|
class="q-mr-md"
|
||||||
|
>
|
||||||
|
{% if LNBITS_SERVICE_FEE_MAX > 0 %}
|
||||||
|
<span
|
||||||
|
v-text='$t("service_fee_max", { amount: "{{ LNBITS_SERVICE_FEE }}", max: "{{ LNBITS_SERVICE_FEE_MAX }}"})'
|
||||||
|
></span>
|
||||||
|
{%else%}
|
||||||
|
<span
|
||||||
|
v-text='$t("service_fee", { amount: "{{ LNBITS_SERVICE_FEE }}" })'
|
||||||
|
></span>
|
||||||
|
{%endif%}
|
||||||
|
<q-tooltip
|
||||||
|
><span v-text='$t("service_fee_tooltip")'></span
|
||||||
|
></q-tooltip>
|
||||||
|
</q-badge>
|
||||||
|
|
||||||
|
{%endif%} {% endblock %}
|
||||||
<q-badge
|
<q-badge
|
||||||
v-if="g.offline"
|
v-if="g.offline"
|
||||||
color="red"
|
color="red"
|
||||||
|
@ -6,6 +6,7 @@ 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_details
|
||||||
from lnbits.core.models import CreateInvoice, Payment
|
from lnbits.core.models import CreateInvoice, Payment
|
||||||
|
from lnbits.core.services import fee_reserve_total
|
||||||
from lnbits.core.views.admin_api import api_auditor
|
from lnbits.core.views.admin_api import api_auditor
|
||||||
from lnbits.core.views.api import api_payment
|
from lnbits.core.views.api import api_payment
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
@ -14,6 +15,7 @@ from lnbits.wallets import get_wallet_class
|
|||||||
from ...helpers import (
|
from ...helpers import (
|
||||||
cancel_invoice,
|
cancel_invoice,
|
||||||
get_random_invoice_data,
|
get_random_invoice_data,
|
||||||
|
get_real_invoice,
|
||||||
is_fake,
|
is_fake,
|
||||||
is_regtest,
|
is_regtest,
|
||||||
pay_real_invoice,
|
pay_real_invoice,
|
||||||
@ -845,3 +847,32 @@ async def test_receive_real_invoice_set_pending_and_check_state(
|
|||||||
assert payment_by_checking_id.pending is False
|
assert payment_by_checking_id.pending is False
|
||||||
assert payment_by_checking_id.bolt11 == payment_not_pending.bolt11
|
assert payment_by_checking_id.bolt11 == payment_not_pending.bolt11
|
||||||
assert payment_by_checking_id.payment_hash == payment_not_pending.payment_hash
|
assert payment_by_checking_id.payment_hash == payment_not_pending.payment_hash
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_check_fee_reserve(client, adminkey_headers_from):
|
||||||
|
# if regtest, create a real invoice, otherwise create an internal invoice
|
||||||
|
# call /api/v1/payments/fee-reserve?invoice=... with it and check if the fee reserve
|
||||||
|
# is correct
|
||||||
|
payment_request = ""
|
||||||
|
if is_regtest:
|
||||||
|
real_invoice = get_real_invoice(1000)
|
||||||
|
payment_request = real_invoice["payment_request"]
|
||||||
|
|
||||||
|
else:
|
||||||
|
create_invoice = CreateInvoice(out=False, amount=1000, memo="test")
|
||||||
|
response = await client.post(
|
||||||
|
"/api/v1/payments",
|
||||||
|
json=create_invoice.dict(),
|
||||||
|
headers=adminkey_headers_from,
|
||||||
|
)
|
||||||
|
assert response.status_code < 300
|
||||||
|
invoice = response.json()
|
||||||
|
payment_request = invoice["payment_request"]
|
||||||
|
|
||||||
|
response = await client.get(
|
||||||
|
f"/api/v1/payments/fee-reserve?invoice={payment_request}",
|
||||||
|
)
|
||||||
|
assert response.status_code < 300
|
||||||
|
fee_reserve = response.json()
|
||||||
|
assert fee_reserve["fee_reserve"] == fee_reserve_total(1000_000)
|
||||||
|
Loading…
Reference in New Issue
Block a user