fix: pay_invoice timeout to prevent blocking (#2875)

* fix: make invoice non blocking

* fix with vlad

* wallet.js take pending into account

* fixup!

* add check payment to outgoing

* revert

* bundle

* fix: label

* fix: rebase change

* feat: configure pay invoice wait time

* fix: check payment button

* bundle

* fix: task in async context

---------

Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
This commit is contained in:
dni ⚡ 2025-02-23 01:02:39 +01:00 committed by GitHub
parent 2f2a3b1092
commit b06c16ed57
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 144 additions and 67 deletions

View file

@ -1,3 +1,4 @@
import asyncio
import json
import time
from datetime import datetime, timedelta, timezone
@ -17,6 +18,7 @@ from lnbits.db import Connection, Filters
from lnbits.decorators import check_user_extension_access
from lnbits.exceptions import InvoiceError, PaymentError
from lnbits.settings import settings
from lnbits.tasks import create_task
from lnbits.utils.crypto import fake_privkey, random_secret_and_hash
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis, satoshis_amount_as_fiat
from lnbits.wallets import fake_wallet, get_funding_source
@ -61,11 +63,11 @@ async def pay_invoice(
invoice = _validate_payment_request(payment_request, max_sat)
assert 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 new_conn:
amount_msat = invoice.amount_msat
wallet = await _check_wallet_for_payment(wallet_id, tag, amount_msat, conn)
wallet = await _check_wallet_for_payment(wallet_id, tag, amount_msat, new_conn)
if await is_internal_status_success(invoice.payment_hash, conn):
if await is_internal_status_success(invoice.payment_hash, new_conn):
raise PaymentError("Internal invoice already paid.", status="failed")
_, extra = await calculate_fiat_amounts(amount_msat / 1000, wallet, extra=extra)
@ -80,8 +82,10 @@ async def pay_invoice(
extra=extra,
)
payment = await _pay_invoice(wallet, create_payment_model, conn)
await _credit_service_fee_wallet(payment, conn)
payment = await _pay_invoice(wallet, create_payment_model, conn)
async with db.reuse_conn(conn) if conn else db.connect() as new_conn:
await _credit_service_fee_wallet(payment, new_conn)
return payment
@ -580,13 +584,19 @@ async def _pay_external_invoice(
fee_reserve_msat = fee_reserve(amount_msat, internal=False)
service_fee_msat = service_fee(amount_msat, internal=False)
funding_source = get_funding_source()
logger.debug(f"fundingsource: sending payment {checking_id}")
payment_response: PaymentResponse = await funding_source.pay_invoice(
create_payment_model.bolt11, fee_reserve_msat
task = create_task(
_fundingsource_pay_invoice(checking_id, payment.bolt11, fee_reserve_msat)
)
logger.debug(f"backend: pay_invoice finished {checking_id}, {payment_response}")
# make sure a hold invoice or deferred payment is not blocking the server
try:
wait_time = max(1, settings.lnbits_funding_source_pay_invoice_wait_seconds)
payment_response = await asyncio.wait_for(task, wait_time)
except asyncio.TimeoutError:
# return pending payment on timeout
logger.debug(f"payment timeout, {checking_id} is still pending")
return payment
if payment_response.checking_id and payment_response.checking_id != checking_id:
logger.warning(
f"backend sent unexpected checking_id (expected: {checking_id} got:"
@ -622,9 +632,22 @@ async def _pay_external_invoice(
"didn't receive checking_id from backend, payment may be stuck in"
f" database: {checking_id}"
)
return payment
async def _fundingsource_pay_invoice(
checking_id: str, bolt11: str, fee_reserve_msat: int
) -> PaymentResponse:
logger.debug(f"fundingsource: sending payment {checking_id}")
funding_source = get_funding_source()
payment_response: PaymentResponse = await funding_source.pay_invoice(
bolt11, fee_reserve_msat
)
logger.debug(f"backend: pay_invoice finished {checking_id}, {payment_response}")
return payment_response
async def _verify_external_payment(
payment: Payment, conn: Optional[Connection] = None
) -> Payment:

View file

@ -54,7 +54,29 @@
</div>
</div>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-4">
<div class="col-12 col-md-3">
<p><span v-text="$t('fee_reserve')"></span></p>
<q-input
type="number"
filled
v-model="formData.lnbits_reserve_fee_min"
:label="$t('fee_reserve_msats')"
>
</q-input>
</div>
<div class="col-12 col-md-3">
<p><span v-text="$t('fee_reserve_percent')"></span></p>
<q-input
type="number"
filled
name="lnbits_reserve_fee_percent"
v-model="formData.lnbits_reserve_fee_percent"
:label="$t('reserve_fee_in_percent')"
step="0.1"
></q-input>
</div>
<div class="col-12 col-md-3">
<p><span v-text="$t('invoice_expiry')"></span></p>
<q-input
filled
@ -65,29 +87,18 @@
>
</q-input>
</div>
<div class="col-12 col-md-8">
<p><span v-text="$t('fee_reserve')"></span></p>
<div class="row q-col-gutter-md">
<div class="col-6">
<q-input
type="number"
filled
v-model="formData.lnbits_reserve_fee_min"
:label="$t('fee_reserve_msats')"
>
</q-input>
</div>
<div class="col-6">
<q-input
type="number"
filled
name="lnbits_reserve_fee_percent"
v-model="formData.lnbits_reserve_fee_percent"
:label="$t('fee_reserve_percent')"
step="0.1"
></q-input>
</div>
</div>
<div class="col-12 col-md-3">
<p><span v-text="$t('payment_wait_time')"></span></p>
<q-input
type="number"
filled
name="lnbits_funding_source_pay_invoice_wait_seconds"
v-model="formData.lnbits_funding_source_pay_invoice_wait_seconds"
:label="$t('payment_wait_time')"
:hint="$t('payment_wait_time_desc')"
step="1"
min="0"
></q-input>
</div>
</div>
<div v-if="isSuperUser">

View file

@ -553,6 +553,9 @@ class FundingSourcesSettings(
BreezSdkFundingSource,
):
lnbits_backend_wallet_class: str = Field(default="VoidWallet")
# How long to wait for the payment to be confirmed before returning a pending status
# It will not fail the payment, it will make it return pending after the timeout
lnbits_funding_source_pay_invoice_wait_seconds: int = Field(default=5)
class WebPushSettings(LNbitsSettings):

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -89,7 +89,7 @@ window.localisation.br = {
pay: 'Pagar',
memo: 'Memo',
date: 'Data',
processing_payment: 'Processando pagamento...',
payment_processing: 'Processando pagamento...',
not_enough_funds: 'Fundos insuficientes!',
search_by_tag_memo_amount: 'Pesquisar por tag, memo, quantidade',
invoice_waiting: 'Fatura aguardando pagamento',

View file

@ -86,7 +86,7 @@ window.localisation.cn = {
pay: '付款',
memo: '备注',
date: '日期',
processing_payment: '正在处理支付...',
payment_processing: '正在处理支付...',
not_enough_funds: '资金不足!',
search_by_tag_memo_amount: '按标签、备注、金额搜索',
invoice_waiting: '待支付的发票',

View file

@ -90,7 +90,7 @@ window.localisation.cs = {
pay: 'Platit',
memo: 'Poznámka',
date: 'Datum',
processing_payment: 'Zpracování platby...',
payment_processing: 'Zpracování platby...',
not_enough_funds: 'Nedostatek prostředků!',
search_by_tag_memo_amount: 'Hledat podle tagu, poznámky, částky',
invoice_waiting: 'Faktura čeká na platbu',

View file

@ -91,7 +91,7 @@ window.localisation.de = {
pay: 'Zahlen',
memo: 'Memo',
date: 'Datum',
processing_payment: 'Zahlung wird verarbeitet ...',
payment_processing: 'Zahlung wird verarbeitet ...',
not_enough_funds: 'Geldmittel sind erschöpft!',
search_by_tag_memo_amount: 'Suche nach Tag, Memo, Betrag',
invoice_waiting: 'Rechnung wartend auf Zahlung',

View file

@ -94,7 +94,11 @@ window.localisation.en = {
memo: 'Memo',
date: 'Date',
path: 'Path',
processing_payment: 'Processing payment...',
payment_processing: 'Processing payment...',
payment_processing: 'Processing payment...',
payment_successful: 'Payment successful!',
payment_pending: 'Payment pending...',
payment_check: 'Check payment',
not_enough_funds: 'Not enough funds!',
search_by_tag_memo_amount: 'Search by tag, memo, amount',
invoice_waiting: 'Invoice waiting to be paid',
@ -411,8 +415,12 @@ window.localisation.en = {
invoice_expiry: 'Invoice Expiry',
invoice_expiry_label: 'Invoice expiry (seconds)',
fee_reserve: 'Fee Reserve',
fee_reserve_percent: 'Fee Reserve Percent',
fee_reserve_msats: 'Reserve fee in msats',
fee_reserve_percent: 'Reserve fee in percent',
reserve_fee_in_percent: 'Reserve fee in percent',
payment_wait_time: 'Payment Wait Time',
payment_wait_time_desc:
'How long to wait when making a payment before marking it as pending. Set higher values for HODL invoices, Boltz, etc.',
server_management: 'Server Management',
base_url: 'Base URL',
base_url_label: 'Static/Base url for the server',

View file

@ -90,7 +90,7 @@ window.localisation.es = {
pay: 'Pagar',
memo: 'Memo',
date: 'Fecha',
processing_payment: 'Procesando pago ...',
payment_processing: 'Procesando pago ...',
not_enough_funds: '¡No hay suficientes fondos!',
search_by_tag_memo_amount: 'Buscar por etiqueta, memo, cantidad',
invoice_waiting: 'Factura esperando pago',

View file

@ -96,7 +96,7 @@ window.localisation.fi = {
memo: 'Kuvaus',
date: 'Päiväys',
path: 'Path',
processing_payment: 'Maksua käsitellään...',
payment_processing: 'Maksua käsitellään...',
not_enough_funds: 'Varat eivät riitä!',
search_by_tag_memo_amount: 'Etsi tunnisteella, muistiolla tai määrällä',
invoice_waiting: 'Lasku osottaa maksamista',

View file

@ -93,7 +93,7 @@ window.localisation.fr = {
pay: 'Payer',
memo: 'Mémo',
date: 'Date',
processing_payment: 'Traitement du paiement...',
payment_processing: 'Traitement du paiement...',
not_enough_funds: 'Fonds insuffisants !',
search_by_tag_memo_amount: 'Rechercher par tag, mémo, montant',
invoice_waiting: 'Facture en attente de paiement',

View file

@ -91,7 +91,7 @@ window.localisation.it = {
pay: 'Paga',
memo: 'Memo',
date: 'Dati',
processing_payment: 'Elaborazione pagamento...',
payment_processing: 'Elaborazione pagamento...',
not_enough_funds: 'Non ci sono abbastanza fondi!',
search_by_tag_memo_amount: 'Cerca per tag, memo, importo...',
invoice_waiting: 'Fattura in attesa di pagamento',

View file

@ -87,7 +87,7 @@ window.localisation.jp = {
pay: '支払う',
memo: 'メモ',
date: '日付',
processing_payment: '支払い処理中',
payment_processing: '支払い処理中',
not_enough_funds: '資金が不足しています',
search_by_tag_memo_amount: 'タグ、メモ、金額で検索',
invoice_waiting: '請求書を待っています',

View file

@ -89,7 +89,7 @@ window.localisation.kr = {
pay: '지불하기',
memo: 'Memo',
date: '일시',
processing_payment: '결제 처리 중...',
payment_processing: '결제 처리 중...',
not_enough_funds: '자금이 부족합니다!',
search_by_tag_memo_amount: '태그, memo, 수량으로 검색하기',
invoice_waiting: '결제를 기다리는 인보이스',

View file

@ -91,7 +91,7 @@ window.localisation.nl = {
pay: 'Betalen',
memo: 'Memo',
date: 'Datum',
processing_payment: 'Verwerking betaling...',
payment_processing: 'Verwerking betaling...',
not_enough_funds: 'Onvoldoende saldo!',
search_by_tag_memo_amount: 'Zoeken op tag, memo, bedrag',
invoice_waiting: 'Factuur wachtend op betaling',

View file

@ -90,7 +90,7 @@ window.localisation.pi = {
pay: 'Pay up or walk the plank, ye scallywag',
memo: 'Message in a bottle, argh',
date: 'Date of the map, me matey',
processing_payment: 'Processing yer payment... don´t make me say it again',
payment_processing: 'Processing yer payment... don´t make me say it again',
not_enough_funds: 'Arrr, ye don´t have enough doubloons! Walk the plank!',
search_by_tag_memo_amount: 'Search by tag, message, or booty amount, savvy',
invoice_waiting: 'Invoice waiting to be plundered, arrr',

View file

@ -89,7 +89,7 @@ window.localisation.pl = {
pay: 'Zapłać',
memo: 'Memo',
date: 'Data',
processing_payment: 'Przetwarzam płatność...',
payment_processing: 'Przetwarzam płatność...',
not_enough_funds: 'Brak wystarczających środków!',
search_by_tag_memo_amount: 'Szukaj po tagu, memo czy wartości',
invoice_waiting: 'Faktura oczekuje na zapłatę',

View file

@ -90,7 +90,7 @@ window.localisation.pt = {
pay: 'Pagar',
memo: 'Memo',
date: 'Data',
processing_payment: 'Processando pagamento...',
payment_processing: 'Processando pagamento...',
not_enough_funds: 'Fundos insuficientes!',
search_by_tag_memo_amount: 'Pesquisar por tag, memo, quantidade',
invoice_waiting: 'Fatura aguardando pagamento',

View file

@ -87,7 +87,7 @@ window.localisation.sk = {
pay: 'Platiť',
memo: 'Poznámka',
date: 'Dátum',
processing_payment: 'Spracovávanie platby...',
payment_processing: 'Spracovávanie platby...',
not_enough_funds: 'Nedostatok prostriedkov!',
search_by_tag_memo_amount: 'Vyhľadať podľa značky, poznámky, sumy',
invoice_waiting: 'Faktúra čakajúca na zaplatenie',

View file

@ -90,7 +90,7 @@ window.localisation.we = {
pay: 'Talu',
memo: 'Memo',
date: 'Dyddiad',
processing_payment: 'Prosesu taliad...',
payment_processing: 'Prosesu taliad...',
not_enough_funds: 'Dim digon o arian!',
search_by_tag_memo_amount: 'Chwilio yn ôl tag, memo, swm',
invoice_waiting: 'Anfoneb yn aros i gael ei thalu',

View file

@ -177,6 +177,26 @@ window.app.component('payment-list', {
LNbits.utils.notifyApiError(err)
})
},
checkPayment(payment_hash) {
LNbits.api
.getPayment(this.g.wallet, payment_hash)
.then(res => {
this.update = !this.update
if (res.data.status == 'success') {
Quasar.Notify.create({
type: 'positive',
message: this.$t('payment_successful')
})
}
if (res.data.status == 'pending') {
Quasar.Notify.create({
type: 'info',
message: this.$t('payment_pending')
})
}
})
.catch(LNbits.utils.notifyApiError)
},
paymentTableRowKey(row) {
return row.payment_hash + row.amount
},

View file

@ -463,22 +463,27 @@ window.WalletPageLogic = {
payInvoice() {
const dismissPaymentMsg = Quasar.Notify.create({
timeout: 0,
message: this.$t('processing_payment')
message: this.$t('payment_processing')
})
LNbits.api
.payInvoice(this.g.wallet, this.parse.data.request)
.then(_ => {
clearInterval(this.parse.paymentChecker)
setTimeout(() => {
clearInterval(this.parse.paymentChecker)
}, 40000)
this.parse.paymentChecker = setInterval(() => {
if (!this.parse.show) {
dismissPaymentMsg()
clearInterval(this.parse.paymentChecker)
}
}, 2000)
.then(response => {
dismissPaymentMsg()
this.updatePayments = !this.updatePayments
this.parse.show = false
if (response.data.status == 'success') {
Quasar.Notify.create({
type: 'positive',
message: this.$t('payment_successful')
})
}
if (response.data.status == 'pending') {
Quasar.Notify.create({
type: 'info',
message: this.$t('payment_pending')
})
}
})
.catch(err => {
dismissPaymentMsg()

View file

@ -949,6 +949,13 @@
@click="copyText(props.row.bolt11)"
:label="$t('copy_invoice')"
></q-btn>
<q-btn
outline
color="grey"
@click="checkPayment(props.row.payment_hash)"
icon="refresh"
:label="$t('payment_check')"
></q-btn>
<q-btn
v-close-popup
flat