feat: HodlInvoices (Rebase) (#2869)
Co-authored-by: pseudozach <git@pseudozach.com>
Co-authored-by: dni ⚡ <office@dnilabs.com>
Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
This commit is contained in:
parent
6e9f451419
commit
f91c933919
23 changed files with 4192 additions and 6027 deletions
|
|
@ -9,6 +9,7 @@ from .misc import (
|
|||
SimpleStatus,
|
||||
)
|
||||
from .payments import (
|
||||
CancelInvoice,
|
||||
CreateInvoice,
|
||||
CreatePayment,
|
||||
DecodePayment,
|
||||
|
|
@ -23,6 +24,7 @@ from .payments import (
|
|||
PaymentsStatusCount,
|
||||
PaymentState,
|
||||
PaymentWalletStats,
|
||||
SettleInvoice,
|
||||
)
|
||||
from .tinyurl import TinyURL
|
||||
from .users import (
|
||||
|
|
@ -57,6 +59,7 @@ __all__ = [
|
|||
"BalanceDelta",
|
||||
"BaseWallet",
|
||||
"Callback",
|
||||
"CancelInvoice",
|
||||
"ConversionData",
|
||||
"CoreAppExtra",
|
||||
"CreateInvoice",
|
||||
|
|
@ -85,6 +88,7 @@ __all__ = [
|
|||
"PaymentsStatusCount",
|
||||
"RegisterUser",
|
||||
"ResetUserPassword",
|
||||
"SettleInvoice",
|
||||
"SimpleStatus",
|
||||
"TinyURL",
|
||||
"UpdateBalance",
|
||||
|
|
|
|||
|
|
@ -252,6 +252,12 @@ class CreateInvoice(BaseModel):
|
|||
memo: str | None = Query(None, max_length=640)
|
||||
description_hash: str | None = None
|
||||
unhashed_description: str | None = None
|
||||
payment_hash: str | None = Query(
|
||||
None,
|
||||
description="The payment hash of the hold invoice.",
|
||||
min_length=64,
|
||||
max_length=64,
|
||||
)
|
||||
expiry: int | None = None
|
||||
extra: dict | None = None
|
||||
webhook: str | None = None
|
||||
|
|
@ -259,6 +265,12 @@ class CreateInvoice(BaseModel):
|
|||
lnurl_callback: str | None = None
|
||||
fiat_provider: str | None = None
|
||||
|
||||
@validator("payment_hash")
|
||||
def check_hex(cls, v):
|
||||
if v:
|
||||
_ = bytes.fromhex(v)
|
||||
return v
|
||||
|
||||
@validator("unit")
|
||||
@classmethod
|
||||
def unit_is_from_allowed_currencies(cls, v):
|
||||
|
|
@ -272,3 +284,31 @@ class PaymentsStatusCount(BaseModel):
|
|||
outgoing: int = 0
|
||||
failed: int = 0
|
||||
pending: int = 0
|
||||
|
||||
|
||||
class SettleInvoice(BaseModel):
|
||||
preimage: str = Field(
|
||||
...,
|
||||
description="The preimage of the payment hash to settle the invoice.",
|
||||
min_length=64,
|
||||
max_length=64,
|
||||
)
|
||||
|
||||
@validator("preimage")
|
||||
def check_hex(cls, v):
|
||||
_ = bytes.fromhex(v)
|
||||
return v
|
||||
|
||||
|
||||
class CancelInvoice(BaseModel):
|
||||
payment_hash: str = Field(
|
||||
...,
|
||||
description="The payment hash of the invoice to cancel.",
|
||||
min_length=64,
|
||||
max_length=64,
|
||||
)
|
||||
|
||||
@validator("payment_hash")
|
||||
def check_hex(cls, v):
|
||||
_ = bytes.fromhex(v)
|
||||
return v
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ from .lnurl import perform_lnurlauth, redeem_lnurl_withdraw
|
|||
from .notifications import enqueue_notification, send_payment_notification
|
||||
from .payments import (
|
||||
calculate_fiat_amounts,
|
||||
cancel_hold_invoice,
|
||||
check_transaction_status,
|
||||
check_wallet_limits,
|
||||
create_fiat_invoice,
|
||||
|
|
@ -17,6 +18,7 @@ from .payments import (
|
|||
get_payments_daily_stats,
|
||||
pay_invoice,
|
||||
service_fee,
|
||||
settle_hold_invoice,
|
||||
update_pending_payment,
|
||||
update_pending_payments,
|
||||
update_wallet_balance,
|
||||
|
|
@ -36,6 +38,7 @@ from .websockets import websocket_manager, websocket_updater
|
|||
|
||||
__all__ = [
|
||||
"calculate_fiat_amounts",
|
||||
"cancel_hold_invoice",
|
||||
"check_admin_settings",
|
||||
"check_transaction_status",
|
||||
"check_wallet_limits",
|
||||
|
|
@ -56,6 +59,7 @@ __all__ = [
|
|||
"redeem_lnurl_withdraw",
|
||||
"send_payment_notification",
|
||||
"service_fee",
|
||||
"settle_hold_invoice",
|
||||
"switch_to_voidwallet",
|
||||
"update_cached_settings",
|
||||
"update_pending_payment",
|
||||
|
|
|
|||
|
|
@ -16,15 +16,16 @@ from lnbits.core.models import PaymentDailyStats, PaymentFilters
|
|||
from lnbits.core.models.payments import CreateInvoice
|
||||
from lnbits.db import Connection, Filters
|
||||
from lnbits.decorators import check_user_extension_access
|
||||
from lnbits.exceptions import InvoiceError, PaymentError
|
||||
from lnbits.exceptions import InvoiceError, PaymentError, UnsupportedError
|
||||
from lnbits.fiat import get_fiat_provider
|
||||
from lnbits.helpers import check_callback_url
|
||||
from lnbits.settings import settings
|
||||
from lnbits.tasks import create_task, internal_invoice_queue_put
|
||||
from lnbits.utils.crypto import fake_privkey, random_secret_and_hash
|
||||
from lnbits.utils.crypto import fake_privkey, random_secret_and_hash, verify_preimage
|
||||
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis, satoshis_amount_as_fiat
|
||||
from lnbits.wallets import fake_wallet, get_funding_source
|
||||
from lnbits.wallets.base import (
|
||||
InvoiceResponse,
|
||||
PaymentPendingStatus,
|
||||
PaymentResponse,
|
||||
PaymentStatus,
|
||||
|
|
@ -200,6 +201,7 @@ async def create_wallet_invoice(wallet_id: str, data: CreateInvoice) -> Payment:
|
|||
extra=data.extra,
|
||||
webhook=data.webhook,
|
||||
internal=data.internal,
|
||||
payment_hash=data.payment_hash,
|
||||
)
|
||||
|
||||
# lnurl_response is not saved in the database
|
||||
|
|
@ -240,6 +242,7 @@ async def create_invoice(
|
|||
extra: Optional[dict] = None,
|
||||
webhook: Optional[str] = None,
|
||||
internal: Optional[bool] = False,
|
||||
payment_hash: str | None = None,
|
||||
conn: Optional[Connection] = None,
|
||||
) -> Payment:
|
||||
if not amount > 0:
|
||||
|
|
@ -273,39 +276,54 @@ async def create_invoice(
|
|||
status="failed",
|
||||
)
|
||||
|
||||
payment_response = await funding_source.create_invoice(
|
||||
amount=amount_sat,
|
||||
memo=invoice_memo,
|
||||
description_hash=description_hash,
|
||||
unhashed_description=unhashed_description,
|
||||
expiry=expiry or settings.lightning_invoice_expiry,
|
||||
)
|
||||
if payment_hash:
|
||||
try:
|
||||
invoice_response = await funding_source.create_hold_invoice(
|
||||
amount=amount_sat,
|
||||
memo=invoice_memo,
|
||||
payment_hash=payment_hash,
|
||||
description_hash=description_hash,
|
||||
)
|
||||
extra["hold_invoice"] = True
|
||||
except UnsupportedError as exc:
|
||||
raise InvoiceError(
|
||||
"Hold invoices are not supported by the funding source.",
|
||||
status="failed",
|
||||
) from exc
|
||||
else:
|
||||
invoice_response = await funding_source.create_invoice(
|
||||
amount=amount_sat,
|
||||
memo=invoice_memo,
|
||||
description_hash=description_hash,
|
||||
unhashed_description=unhashed_description,
|
||||
expiry=expiry or settings.lightning_invoice_expiry,
|
||||
)
|
||||
if (
|
||||
not payment_response.ok
|
||||
or not payment_response.payment_request
|
||||
or not payment_response.checking_id
|
||||
not invoice_response.ok
|
||||
or not invoice_response.payment_request
|
||||
or not invoice_response.checking_id
|
||||
):
|
||||
raise InvoiceError(
|
||||
message=payment_response.error_message or "unexpected backend error.",
|
||||
message=invoice_response.error_message or "unexpected backend error.",
|
||||
status="pending",
|
||||
)
|
||||
invoice = bolt11_decode(payment_response.payment_request)
|
||||
invoice = bolt11_decode(invoice_response.payment_request)
|
||||
|
||||
create_payment_model = CreatePayment(
|
||||
wallet_id=wallet_id,
|
||||
bolt11=payment_response.payment_request,
|
||||
bolt11=invoice_response.payment_request,
|
||||
payment_hash=invoice.payment_hash,
|
||||
preimage=payment_response.preimage,
|
||||
preimage=invoice_response.preimage,
|
||||
amount_msat=amount_sat * 1000,
|
||||
expiry=invoice.expiry_date,
|
||||
memo=memo,
|
||||
extra=extra,
|
||||
webhook=webhook,
|
||||
fee=payment_response.fee_msat or 0,
|
||||
fee=invoice_response.fee_msat or 0,
|
||||
)
|
||||
|
||||
payment = await create_payment(
|
||||
checking_id=payment_response.checking_id,
|
||||
checking_id=invoice_response.checking_id,
|
||||
data=create_payment_model,
|
||||
conn=conn,
|
||||
)
|
||||
|
|
@ -949,3 +967,40 @@ async def _check_fiat_invoice_limits(
|
|||
f"The amount exceeds the '{fiat_provider_name}'"
|
||||
"faucet wallet balance.",
|
||||
)
|
||||
|
||||
|
||||
async def settle_hold_invoice(payment: Payment, preimage: str) -> InvoiceResponse:
|
||||
if verify_preimage(preimage, payment.payment_hash) is False:
|
||||
raise InvoiceError("Invalid preimage.", status="failed")
|
||||
|
||||
funding_source = get_funding_source()
|
||||
response = await funding_source.settle_hold_invoice(preimage=preimage)
|
||||
|
||||
if not response.ok:
|
||||
raise InvoiceError(
|
||||
response.error_message or "Unexpected backend error.", status="failed"
|
||||
)
|
||||
|
||||
payment.preimage = preimage
|
||||
payment.extra["hold_invoice_settled"] = True
|
||||
await update_payment(payment)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
async def cancel_hold_invoice(payment: Payment) -> InvoiceResponse:
|
||||
funding_source = get_funding_source()
|
||||
response = await funding_source.cancel_hold_invoice(
|
||||
payment_hash=payment.payment_hash
|
||||
)
|
||||
|
||||
if not response.ok:
|
||||
raise InvoiceError(
|
||||
response.error_message or "Unexpected backend error.", status="failed"
|
||||
)
|
||||
|
||||
payment.status = PaymentState.FAILED
|
||||
payment.extra["hold_invoice_cancelled"] = True
|
||||
await update_payment(payment)
|
||||
|
||||
return response
|
||||
|
|
|
|||
|
|
@ -661,7 +661,12 @@
|
|||
:readonly="receive.lnurl && receive.lnurl.fixed"
|
||||
></q-input>
|
||||
{% endif %}
|
||||
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model="receive.data.payment_hash"
|
||||
:label="$t('hold_invoice_payment_hash')"
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import json
|
||||
import ssl
|
||||
from hashlib import sha256
|
||||
from http import HTTPStatus
|
||||
from math import ceil
|
||||
from typing import Optional
|
||||
|
|
@ -22,6 +23,7 @@ from lnbits.core.crud.payments import (
|
|||
get_wallets_stats,
|
||||
)
|
||||
from lnbits.core.models import (
|
||||
CancelInvoice,
|
||||
CreateInvoice,
|
||||
CreateLnurl,
|
||||
DecodePayment,
|
||||
|
|
@ -34,6 +36,7 @@ from lnbits.core.models import (
|
|||
PaymentFilters,
|
||||
PaymentHistoryPoint,
|
||||
PaymentWalletStats,
|
||||
SettleInvoice,
|
||||
)
|
||||
from lnbits.core.models.users import User
|
||||
from lnbits.db import Filters, Page
|
||||
|
|
@ -52,6 +55,7 @@ from lnbits.helpers import (
|
|||
from lnbits.lnurl import decode as lnurl_decode
|
||||
from lnbits.settings import settings
|
||||
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis
|
||||
from lnbits.wallets.base import InvoiceResponse
|
||||
|
||||
from ..crud import (
|
||||
DateTrunc,
|
||||
|
|
@ -62,10 +66,12 @@ from ..crud import (
|
|||
get_wallet_for_key,
|
||||
)
|
||||
from ..services import (
|
||||
cancel_hold_invoice,
|
||||
create_payment_request,
|
||||
fee_reserve_total,
|
||||
get_payments_daily_stats,
|
||||
pay_invoice,
|
||||
settle_hold_invoice,
|
||||
update_pending_payment,
|
||||
update_pending_payments,
|
||||
)
|
||||
|
|
@ -476,3 +482,34 @@ async def api_payment_pay_with_nfc(
|
|||
|
||||
except Exception as e:
|
||||
return JSONResponse({"success": False, "detail": f"Unexpected error: {e}"})
|
||||
|
||||
|
||||
@payment_router.post("/settle")
|
||||
async def api_payments_settle(
|
||||
data: SettleInvoice, key_type: WalletTypeInfo = Depends(require_admin_key)
|
||||
) -> InvoiceResponse:
|
||||
payment_hash = sha256(bytes.fromhex(data.preimage)).hexdigest()
|
||||
payment = await get_standalone_payment(
|
||||
payment_hash, incoming=True, wallet_id=key_type.wallet.id
|
||||
)
|
||||
if not payment:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND,
|
||||
detail="Payment does not exist or does not belong to this wallet.",
|
||||
)
|
||||
return await settle_hold_invoice(payment, data.preimage)
|
||||
|
||||
|
||||
@payment_router.post("/cancel")
|
||||
async def api_payments_cancel(
|
||||
data: CancelInvoice, key_type: WalletTypeInfo = Depends(require_admin_key)
|
||||
) -> InvoiceResponse:
|
||||
payment = await get_standalone_payment(
|
||||
data.payment_hash, incoming=True, wallet_id=key_type.wallet.id
|
||||
)
|
||||
if not payment:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND,
|
||||
detail="Payment does not exist or does not belong to this wallet.",
|
||||
)
|
||||
return await cancel_hold_invoice(payment)
|
||||
|
|
|
|||
2
lnbits/static/bundle-components.min.js
vendored
2
lnbits/static/bundle-components.min.js
vendored
File diff suppressed because one or more lines are too long
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
|
|
@ -176,7 +176,17 @@ window.localisation.en = {
|
|||
extension_required_lnbits_version: 'This release requires LNbits version',
|
||||
min_version: 'Minimum (included)',
|
||||
max_version: 'Maximum (excluded)',
|
||||
preimage: 'Preimage',
|
||||
preimage_hint: 'Preimage to settle the hold invoice',
|
||||
hold_invoice: 'Hold Invoice',
|
||||
hold_invoice_description:
|
||||
'This invoice is on hold and requires a preimage to settle.',
|
||||
payment_hash: 'Payment Hash',
|
||||
invoice_cancelled: 'Invoice Cancelled',
|
||||
invoice_settled: 'Invoice Settled',
|
||||
hold_invoice_payment_hash: 'Payment hash for hold invoice (optional)',
|
||||
settle_invoice: 'Settle Invoice',
|
||||
cancel_invoice: 'Cancel Invoice',
|
||||
fee: 'Fee',
|
||||
amount: 'Amount',
|
||||
amount_limits: 'Amount Limits',
|
||||
|
|
|
|||
|
|
@ -21,7 +21,8 @@ window.LNbits = {
|
|||
unit = 'sat',
|
||||
lnurlCallback = null,
|
||||
fiatProvider = null,
|
||||
internalMemo = null
|
||||
internalMemo = null,
|
||||
payment_hash = null
|
||||
) {
|
||||
const data = {
|
||||
out: false,
|
||||
|
|
@ -29,7 +30,8 @@ window.LNbits = {
|
|||
memo: memo,
|
||||
unit: unit,
|
||||
lnurl_callback: lnurlCallback,
|
||||
fiat_provider: fiatProvider
|
||||
fiat_provider: fiatProvider,
|
||||
payment_hash: payment_hash
|
||||
}
|
||||
if (internalMemo) {
|
||||
data.extra = {
|
||||
|
|
@ -80,6 +82,16 @@ window.LNbits = {
|
|||
data
|
||||
)
|
||||
},
|
||||
cancelInvoice(wallet, paymentHash) {
|
||||
return this.request('post', '/api/v1/payments/cancel', wallet.adminkey, {
|
||||
payment_hash: paymentHash
|
||||
})
|
||||
},
|
||||
settleInvoice(wallet, preimage) {
|
||||
return this.request('post', `/api/v1/payments/settle`, wallet.adminkey, {
|
||||
preimage: preimage
|
||||
})
|
||||
},
|
||||
authLnurl(wallet, callback) {
|
||||
return this.request('post', '/api/v1/lnurlauth', wallet.adminkey, {
|
||||
callback
|
||||
|
|
|
|||
|
|
@ -118,7 +118,13 @@ window.app.component('payment-list', {
|
|||
field: row => row.extra.wallet_fiat_amount
|
||||
}
|
||||
],
|
||||
preimage: null,
|
||||
loading: false
|
||||
},
|
||||
hodlInvoice: {
|
||||
show: false,
|
||||
payment: null,
|
||||
preimage: null
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -223,6 +229,35 @@ window.app.component('payment-list', {
|
|||
})
|
||||
.catch(LNbits.utils.notifyApiError)
|
||||
},
|
||||
showHoldInvoiceDialog(payment) {
|
||||
this.hodlInvoice.show = true
|
||||
this.hodlInvoice.preimage = ''
|
||||
this.hodlInvoice.payment = payment
|
||||
},
|
||||
cancelHoldInvoice(payment_hash) {
|
||||
LNbits.api
|
||||
.cancelInvoice(this.g.wallet, payment_hash)
|
||||
.then(() => {
|
||||
this.update = !this.update
|
||||
Quasar.Notify.create({
|
||||
type: 'positive',
|
||||
message: this.$t('invoice_cancelled')
|
||||
})
|
||||
})
|
||||
.catch(LNbits.utils.notifyApiError)
|
||||
},
|
||||
settleHoldInvoice(preimage) {
|
||||
LNbits.api
|
||||
.settleInvoice(this.g.wallet, preimage)
|
||||
.then(() => {
|
||||
this.update = !this.update
|
||||
Quasar.Notify.create({
|
||||
type: 'positive',
|
||||
message: this.$t('invoice_settled')
|
||||
})
|
||||
})
|
||||
.catch(LNbits.utils.notifyApiError)
|
||||
},
|
||||
paymentTableRowKey(row) {
|
||||
return row.payment_hash + row.amount
|
||||
},
|
||||
|
|
|
|||
|
|
@ -40,7 +40,8 @@ window.WalletPageLogic = {
|
|||
data: {
|
||||
amount: null,
|
||||
memo: '',
|
||||
internalMemo: null
|
||||
internalMemo: null,
|
||||
payment_hash: null
|
||||
}
|
||||
},
|
||||
invoiceQrCode: '',
|
||||
|
|
@ -200,6 +201,7 @@ window.WalletPageLogic = {
|
|||
this.receive.data.amount = null
|
||||
this.receive.data.memo = null
|
||||
this.receive.data.internalMemo = null
|
||||
this.receive.data.payment_hash = null
|
||||
this.receive.unit = this.isFiatPriority
|
||||
? this.g.wallet.currency || 'sat'
|
||||
: 'sat'
|
||||
|
|
@ -258,7 +260,8 @@ window.WalletPageLogic = {
|
|||
this.receive.unit,
|
||||
this.receive.lnurl && this.receive.lnurl.callback,
|
||||
this.receive.fiatProvider,
|
||||
this.receive.data.internalMemo
|
||||
this.receive.data.internalMemo,
|
||||
this.receive.data.payment_hash
|
||||
)
|
||||
.then(response => {
|
||||
this.g.updatePayments = !this.g.updatePayments
|
||||
|
|
|
|||
|
|
@ -880,6 +880,19 @@
|
|||
:props="props"
|
||||
style="white-space: normal; word-break: break-all"
|
||||
>
|
||||
<q-icon
|
||||
v-if="
|
||||
props.row.isIn &&
|
||||
props.row.isPending &&
|
||||
props.row.extra.hold_invoice
|
||||
"
|
||||
name="pause_presentation"
|
||||
color="grey"
|
||||
class="cursor-pointer q-mr-sm"
|
||||
@click="showHoldInvoiceDialog(props.row)"
|
||||
>
|
||||
<q-tooltip><span v-text="$t('hold_invoice')"></span></q-tooltip>
|
||||
</q-icon>
|
||||
<q-badge
|
||||
v-if="props.row.tag"
|
||||
color="yellow"
|
||||
|
|
@ -941,7 +954,7 @@
|
|||
</q-td>
|
||||
<q-dialog v-model="props.expand" :props="props" position="top">
|
||||
<q-card class="q-pa-sm q-pt-xl lnbits__dialog-card">
|
||||
<q-card-section class="">
|
||||
<q-card-section>
|
||||
<q-list bordered separator>
|
||||
<q-expansion-item
|
||||
expand-separator
|
||||
|
|
@ -1004,6 +1017,7 @@
|
|||
></lnbits-payment-details>
|
||||
</q-expansion-item>
|
||||
</q-list>
|
||||
|
||||
<div
|
||||
v-if="props.row.isIn && props.row.isPending && props.row.bolt11"
|
||||
class="text-center q-my-lg"
|
||||
|
|
@ -1060,6 +1074,55 @@
|
|||
</q-card-section>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
<q-dialog v-model="hodlInvoice.show" position="top">
|
||||
<q-card class="q-pa-sm q-pt-xl lnbits__dialog-card">
|
||||
<q-card-section>
|
||||
<q-item-label class="text-h6">
|
||||
<span v-text="$t('hold_invoice')"></span>
|
||||
</q-item-label>
|
||||
<q-item-label class="text-subtitle2">
|
||||
<span v-text="$t('hold_invoice_description')"></span>
|
||||
</q-item-label>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-input
|
||||
filled
|
||||
:label="$t('preimage')"
|
||||
:hint="$t('preimage_hint')"
|
||||
v-model="hodlInvoice.preimage"
|
||||
dense
|
||||
autofocus
|
||||
@keyup.enter="settleHoldInvoice(hodlInvoice.preimage)"
|
||||
>
|
||||
</q-input>
|
||||
</q-card-section>
|
||||
<q-card-section class="row q-gutter-x-sm">
|
||||
<q-btn
|
||||
@click="settleHoldInvoice(hodlInvoice.preimage)"
|
||||
outline
|
||||
v-close-popup
|
||||
color="grey"
|
||||
:label="$t('settle_invoice')"
|
||||
>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
v-close-popup
|
||||
outline
|
||||
color="grey"
|
||||
class="q-ml-sm"
|
||||
@click="cancelHoldInvoice(hodlInvoice.payment.payment_hash)"
|
||||
:label="$t('cancel_invoice')"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
v-close-popup
|
||||
flat
|
||||
color="grey"
|
||||
class="q-ml-auto"
|
||||
:label="$t('close')"
|
||||
></q-btn>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ from typing import TYPE_CHECKING, NamedTuple
|
|||
|
||||
from loguru import logger
|
||||
|
||||
from lnbits.exceptions import InvoiceError
|
||||
from lnbits.settings import settings
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -152,6 +153,29 @@ class Wallet(ABC):
|
|||
) -> Coroutine[None, None, PaymentStatus]:
|
||||
pass
|
||||
|
||||
async def create_hold_invoice(
|
||||
self,
|
||||
amount: int,
|
||||
payment_hash: str,
|
||||
memo: str | None = None,
|
||||
description_hash: bytes | None = None,
|
||||
unhashed_description: bytes | None = None,
|
||||
**kwargs,
|
||||
) -> InvoiceResponse:
|
||||
raise InvoiceError(
|
||||
message="Hold invoices are not supported by this wallet.", status="failed"
|
||||
)
|
||||
|
||||
async def settle_hold_invoice(self, preimage: str) -> InvoiceResponse:
|
||||
raise InvoiceError(
|
||||
message="Hold invoices are not supported by this wallet.", status="failed"
|
||||
)
|
||||
|
||||
async def cancel_hold_invoice(self, payment_hash: str) -> InvoiceResponse:
|
||||
raise InvoiceError(
|
||||
message="Hold invoices are not supported by this wallet.", status="failed"
|
||||
)
|
||||
|
||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
while settings.lnbits_running:
|
||||
for invoice in self.pending_invoices:
|
||||
|
|
|
|||
65
lnbits/wallets/lnd_grpc_files/invoices_pb2.py
Normal file
65
lnbits/wallets/lnd_grpc_files/invoices_pb2.py
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
# NO CHECKED-IN PROTOBUF GENCODE
|
||||
# source: invoices.proto
|
||||
# Protobuf Python Version: 5.28.1
|
||||
"""Generated protocol buffer code."""
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||
from google.protobuf import runtime_version as _runtime_version
|
||||
from google.protobuf import symbol_database as _symbol_database
|
||||
from google.protobuf.internal import builder as _builder
|
||||
_runtime_version.ValidateProtobufRuntimeVersion(
|
||||
_runtime_version.Domain.PUBLIC,
|
||||
5,
|
||||
28,
|
||||
1,
|
||||
'',
|
||||
'invoices.proto'
|
||||
)
|
||||
# @@protoc_insertion_point(imports)
|
||||
|
||||
_sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
import lnbits.wallets.lnd_grpc_files.lightning_pb2 as lightning__pb2
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0einvoices.proto\x12\x0binvoicesrpc\x1a\x0flightning.proto\"(\n\x10\x43\x61ncelInvoiceMsg\x12\x14\n\x0cpayment_hash\x18\x01 \x01(\x0c\"\x13\n\x11\x43\x61ncelInvoiceResp\"\xe4\x01\n\x15\x41\x64\x64HoldInvoiceRequest\x12\x0c\n\x04memo\x18\x01 \x01(\t\x12\x0c\n\x04hash\x18\x02 \x01(\x0c\x12\r\n\x05value\x18\x03 \x01(\x03\x12\x12\n\nvalue_msat\x18\n \x01(\x03\x12\x18\n\x10\x64\x65scription_hash\x18\x04 \x01(\x0c\x12\x0e\n\x06\x65xpiry\x18\x05 \x01(\x03\x12\x15\n\rfallback_addr\x18\x06 \x01(\t\x12\x13\n\x0b\x63ltv_expiry\x18\x07 \x01(\x04\x12%\n\x0broute_hints\x18\x08 \x03(\x0b\x32\x10.lnrpc.RouteHint\x12\x0f\n\x07private\x18\t \x01(\x08\"V\n\x12\x41\x64\x64HoldInvoiceResp\x12\x17\n\x0fpayment_request\x18\x01 \x01(\t\x12\x11\n\tadd_index\x18\x02 \x01(\x04\x12\x14\n\x0cpayment_addr\x18\x03 \x01(\x0c\"$\n\x10SettleInvoiceMsg\x12\x10\n\x08preimage\x18\x01 \x01(\x0c\"\x13\n\x11SettleInvoiceResp\"5\n\x1dSubscribeSingleInvoiceRequest\x12\x0e\n\x06r_hash\x18\x02 \x01(\x0cJ\x04\x08\x01\x10\x02\"\x99\x01\n\x10LookupInvoiceMsg\x12\x16\n\x0cpayment_hash\x18\x01 \x01(\x0cH\x00\x12\x16\n\x0cpayment_addr\x18\x02 \x01(\x0cH\x00\x12\x10\n\x06set_id\x18\x03 \x01(\x0cH\x00\x12\x34\n\x0flookup_modifier\x18\x04 \x01(\x0e\x32\x1b.invoicesrpc.LookupModifierB\r\n\x0binvoice_ref\".\n\nCircuitKey\x12\x0f\n\x07\x63han_id\x18\x01 \x01(\x04\x12\x0f\n\x07htlc_id\x18\x02 \x01(\x04\"\xdd\x02\n\x11HtlcModifyRequest\x12\x1f\n\x07invoice\x18\x01 \x01(\x0b\x32\x0e.lnrpc.Invoice\x12\x36\n\x15\x65xit_htlc_circuit_key\x18\x02 \x01(\x0b\x32\x17.invoicesrpc.CircuitKey\x12\x15\n\rexit_htlc_amt\x18\x03 \x01(\x04\x12\x18\n\x10\x65xit_htlc_expiry\x18\x04 \x01(\r\x12\x16\n\x0e\x63urrent_height\x18\x05 \x01(\r\x12\x64\n\x1d\x65xit_htlc_wire_custom_records\x18\x06 \x03(\x0b\x32=.invoicesrpc.HtlcModifyRequest.ExitHtlcWireCustomRecordsEntry\x1a@\n\x1e\x45xitHtlcWireCustomRecordsEntry\x12\x0b\n\x03key\x18\x01 \x01(\x04\x12\r\n\x05value\x18\x02 \x01(\x0c:\x02\x38\x01\"z\n\x12HtlcModifyResponse\x12,\n\x0b\x63ircuit_key\x18\x01 \x01(\x0b\x32\x17.invoicesrpc.CircuitKey\x12\x15\n\x08\x61mt_paid\x18\x02 \x01(\x04H\x00\x88\x01\x01\x12\x12\n\ncancel_set\x18\x03 \x01(\x08\x42\x0b\n\t_amt_paid*D\n\x0eLookupModifier\x12\x0b\n\x07\x44\x45\x46\x41ULT\x10\x00\x12\x11\n\rHTLC_SET_ONLY\x10\x01\x12\x12\n\x0eHTLC_SET_BLANK\x10\x02\x32\xf0\x03\n\x08Invoices\x12V\n\x16SubscribeSingleInvoice\x12*.invoicesrpc.SubscribeSingleInvoiceRequest\x1a\x0e.lnrpc.Invoice0\x01\x12N\n\rCancelInvoice\x12\x1d.invoicesrpc.CancelInvoiceMsg\x1a\x1e.invoicesrpc.CancelInvoiceResp\x12U\n\x0e\x41\x64\x64HoldInvoice\x12\".invoicesrpc.AddHoldInvoiceRequest\x1a\x1f.invoicesrpc.AddHoldInvoiceResp\x12N\n\rSettleInvoice\x12\x1d.invoicesrpc.SettleInvoiceMsg\x1a\x1e.invoicesrpc.SettleInvoiceResp\x12@\n\x0fLookupInvoiceV2\x12\x1d.invoicesrpc.LookupInvoiceMsg\x1a\x0e.lnrpc.Invoice\x12S\n\x0cHtlcModifier\x12\x1f.invoicesrpc.HtlcModifyResponse\x1a\x1e.invoicesrpc.HtlcModifyRequest(\x01\x30\x01\x42\x33Z1github.com/lightningnetwork/lnd/lnrpc/invoicesrpcb\x06proto3')
|
||||
|
||||
_globals = globals()
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'invoices_pb2', _globals)
|
||||
if not _descriptor._USE_C_DESCRIPTORS:
|
||||
_globals['DESCRIPTOR']._loaded_options = None
|
||||
_globals['DESCRIPTOR']._serialized_options = b'Z1github.com/lightningnetwork/lnd/lnrpc/invoicesrpc'
|
||||
_globals['_HTLCMODIFYREQUEST_EXITHTLCWIRECUSTOMRECORDSENTRY']._loaded_options = None
|
||||
_globals['_HTLCMODIFYREQUEST_EXITHTLCWIRECUSTOMRECORDSENTRY']._serialized_options = b'8\001'
|
||||
_globals['_LOOKUPMODIFIER']._serialized_start=1224
|
||||
_globals['_LOOKUPMODIFIER']._serialized_end=1292
|
||||
_globals['_CANCELINVOICEMSG']._serialized_start=48
|
||||
_globals['_CANCELINVOICEMSG']._serialized_end=88
|
||||
_globals['_CANCELINVOICERESP']._serialized_start=90
|
||||
_globals['_CANCELINVOICERESP']._serialized_end=109
|
||||
_globals['_ADDHOLDINVOICEREQUEST']._serialized_start=112
|
||||
_globals['_ADDHOLDINVOICEREQUEST']._serialized_end=340
|
||||
_globals['_ADDHOLDINVOICERESP']._serialized_start=342
|
||||
_globals['_ADDHOLDINVOICERESP']._serialized_end=428
|
||||
_globals['_SETTLEINVOICEMSG']._serialized_start=430
|
||||
_globals['_SETTLEINVOICEMSG']._serialized_end=466
|
||||
_globals['_SETTLEINVOICERESP']._serialized_start=468
|
||||
_globals['_SETTLEINVOICERESP']._serialized_end=487
|
||||
_globals['_SUBSCRIBESINGLEINVOICEREQUEST']._serialized_start=489
|
||||
_globals['_SUBSCRIBESINGLEINVOICEREQUEST']._serialized_end=542
|
||||
_globals['_LOOKUPINVOICEMSG']._serialized_start=545
|
||||
_globals['_LOOKUPINVOICEMSG']._serialized_end=698
|
||||
_globals['_CIRCUITKEY']._serialized_start=700
|
||||
_globals['_CIRCUITKEY']._serialized_end=746
|
||||
_globals['_HTLCMODIFYREQUEST']._serialized_start=749
|
||||
_globals['_HTLCMODIFYREQUEST']._serialized_end=1098
|
||||
_globals['_HTLCMODIFYREQUEST_EXITHTLCWIRECUSTOMRECORDSENTRY']._serialized_start=1034
|
||||
_globals['_HTLCMODIFYREQUEST_EXITHTLCWIRECUSTOMRECORDSENTRY']._serialized_end=1098
|
||||
_globals['_HTLCMODIFYRESPONSE']._serialized_start=1100
|
||||
_globals['_HTLCMODIFYRESPONSE']._serialized_end=1222
|
||||
_globals['_INVOICES']._serialized_start=1295
|
||||
_globals['_INVOICES']._serialized_end=1791
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
392
lnbits/wallets/lnd_grpc_files/invoices_pb2_grpc.py
Normal file
392
lnbits/wallets/lnd_grpc_files/invoices_pb2_grpc.py
Normal file
|
|
@ -0,0 +1,392 @@
|
|||
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
|
||||
"""Client and server classes corresponding to protobuf-defined services."""
|
||||
import grpc
|
||||
import warnings
|
||||
|
||||
import lnbits.wallets.lnd_grpc_files.invoices_pb2 as invoices__pb2
|
||||
import lnbits.wallets.lnd_grpc_files.lightning_pb2 as lightning__pb2
|
||||
|
||||
GRPC_GENERATED_VERSION = '1.68.1'
|
||||
GRPC_VERSION = grpc.__version__
|
||||
_version_not_supported = False
|
||||
|
||||
try:
|
||||
from grpc._utilities import first_version_is_lower
|
||||
_version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
|
||||
except ImportError:
|
||||
_version_not_supported = True
|
||||
|
||||
if _version_not_supported:
|
||||
raise RuntimeError(
|
||||
f'The grpc package installed is at version {GRPC_VERSION},'
|
||||
+ f' but the generated code in invoices_pb2_grpc.py depends on'
|
||||
+ f' grpcio>={GRPC_GENERATED_VERSION}.'
|
||||
+ f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
|
||||
+ f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
|
||||
)
|
||||
|
||||
|
||||
class InvoicesStub(object):
|
||||
"""
|
||||
Comments in this file will be directly parsed into the API
|
||||
Documentation as descriptions of the associated method, message, or field.
|
||||
These descriptions should go right above the definition of the object, and
|
||||
can be in either block or // comment format.
|
||||
|
||||
An RPC method can be matched to an lncli command by placing a line in the
|
||||
beginning of the description in exactly the following format:
|
||||
lncli: `methodname`
|
||||
|
||||
Failure to specify the exact name of the command will cause documentation
|
||||
generation to fail.
|
||||
|
||||
More information on how exactly the gRPC documentation is generated from
|
||||
this proto file can be found here:
|
||||
https://github.com/lightninglabs/lightning-api
|
||||
|
||||
Invoices is a service that can be used to create, accept, settle and cancel
|
||||
invoices.
|
||||
"""
|
||||
|
||||
def __init__(self, channel):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
channel: A grpc.Channel.
|
||||
"""
|
||||
self.SubscribeSingleInvoice = channel.unary_stream(
|
||||
'/invoicesrpc.Invoices/SubscribeSingleInvoice',
|
||||
request_serializer=invoices__pb2.SubscribeSingleInvoiceRequest.SerializeToString,
|
||||
response_deserializer=lightning__pb2.Invoice.FromString,
|
||||
_registered_method=True)
|
||||
self.CancelInvoice = channel.unary_unary(
|
||||
'/invoicesrpc.Invoices/CancelInvoice',
|
||||
request_serializer=invoices__pb2.CancelInvoiceMsg.SerializeToString,
|
||||
response_deserializer=invoices__pb2.CancelInvoiceResp.FromString,
|
||||
_registered_method=True)
|
||||
self.AddHoldInvoice = channel.unary_unary(
|
||||
'/invoicesrpc.Invoices/AddHoldInvoice',
|
||||
request_serializer=invoices__pb2.AddHoldInvoiceRequest.SerializeToString,
|
||||
response_deserializer=invoices__pb2.AddHoldInvoiceResp.FromString,
|
||||
_registered_method=True)
|
||||
self.SettleInvoice = channel.unary_unary(
|
||||
'/invoicesrpc.Invoices/SettleInvoice',
|
||||
request_serializer=invoices__pb2.SettleInvoiceMsg.SerializeToString,
|
||||
response_deserializer=invoices__pb2.SettleInvoiceResp.FromString,
|
||||
_registered_method=True)
|
||||
self.LookupInvoiceV2 = channel.unary_unary(
|
||||
'/invoicesrpc.Invoices/LookupInvoiceV2',
|
||||
request_serializer=invoices__pb2.LookupInvoiceMsg.SerializeToString,
|
||||
response_deserializer=lightning__pb2.Invoice.FromString,
|
||||
_registered_method=True)
|
||||
self.HtlcModifier = channel.stream_stream(
|
||||
'/invoicesrpc.Invoices/HtlcModifier',
|
||||
request_serializer=invoices__pb2.HtlcModifyResponse.SerializeToString,
|
||||
response_deserializer=invoices__pb2.HtlcModifyRequest.FromString,
|
||||
_registered_method=True)
|
||||
|
||||
|
||||
class InvoicesServicer(object):
|
||||
"""
|
||||
Comments in this file will be directly parsed into the API
|
||||
Documentation as descriptions of the associated method, message, or field.
|
||||
These descriptions should go right above the definition of the object, and
|
||||
can be in either block or // comment format.
|
||||
|
||||
An RPC method can be matched to an lncli command by placing a line in the
|
||||
beginning of the description in exactly the following format:
|
||||
lncli: `methodname`
|
||||
|
||||
Failure to specify the exact name of the command will cause documentation
|
||||
generation to fail.
|
||||
|
||||
More information on how exactly the gRPC documentation is generated from
|
||||
this proto file can be found here:
|
||||
https://github.com/lightninglabs/lightning-api
|
||||
|
||||
Invoices is a service that can be used to create, accept, settle and cancel
|
||||
invoices.
|
||||
"""
|
||||
|
||||
def SubscribeSingleInvoice(self, request, context):
|
||||
"""
|
||||
SubscribeSingleInvoice returns a uni-directional stream (server -> client)
|
||||
to notify the client of state transitions of the specified invoice.
|
||||
Initially the current invoice state is always sent out.
|
||||
"""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
def CancelInvoice(self, request, context):
|
||||
"""lncli: `cancelinvoice`
|
||||
CancelInvoice cancels a currently open invoice. If the invoice is already
|
||||
canceled, this call will succeed. If the invoice is already settled, it will
|
||||
fail.
|
||||
"""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
def AddHoldInvoice(self, request, context):
|
||||
"""lncli: `addholdinvoice`
|
||||
AddHoldInvoice creates a hold invoice. It ties the invoice to the hash
|
||||
supplied in the request.
|
||||
"""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
def SettleInvoice(self, request, context):
|
||||
"""lncli: `settleinvoice`
|
||||
SettleInvoice settles an accepted invoice. If the invoice is already
|
||||
settled, this call will succeed.
|
||||
"""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
def LookupInvoiceV2(self, request, context):
|
||||
"""
|
||||
LookupInvoiceV2 attempts to look up at invoice. An invoice can be referenced
|
||||
using either its payment hash, payment address, or set ID.
|
||||
"""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
def HtlcModifier(self, request_iterator, context):
|
||||
"""
|
||||
HtlcModifier is a bidirectional streaming RPC that allows a client to
|
||||
intercept and modify the HTLCs that attempt to settle the given invoice. The
|
||||
server will send HTLCs of invoices to the client and the client can modify
|
||||
some aspects of the HTLC in order to pass the invoice acceptance tests.
|
||||
"""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
|
||||
def add_InvoicesServicer_to_server(servicer, server):
|
||||
rpc_method_handlers = {
|
||||
'SubscribeSingleInvoice': grpc.unary_stream_rpc_method_handler(
|
||||
servicer.SubscribeSingleInvoice,
|
||||
request_deserializer=invoices__pb2.SubscribeSingleInvoiceRequest.FromString,
|
||||
response_serializer=lightning__pb2.Invoice.SerializeToString,
|
||||
),
|
||||
'CancelInvoice': grpc.unary_unary_rpc_method_handler(
|
||||
servicer.CancelInvoice,
|
||||
request_deserializer=invoices__pb2.CancelInvoiceMsg.FromString,
|
||||
response_serializer=invoices__pb2.CancelInvoiceResp.SerializeToString,
|
||||
),
|
||||
'AddHoldInvoice': grpc.unary_unary_rpc_method_handler(
|
||||
servicer.AddHoldInvoice,
|
||||
request_deserializer=invoices__pb2.AddHoldInvoiceRequest.FromString,
|
||||
response_serializer=invoices__pb2.AddHoldInvoiceResp.SerializeToString,
|
||||
),
|
||||
'SettleInvoice': grpc.unary_unary_rpc_method_handler(
|
||||
servicer.SettleInvoice,
|
||||
request_deserializer=invoices__pb2.SettleInvoiceMsg.FromString,
|
||||
response_serializer=invoices__pb2.SettleInvoiceResp.SerializeToString,
|
||||
),
|
||||
'LookupInvoiceV2': grpc.unary_unary_rpc_method_handler(
|
||||
servicer.LookupInvoiceV2,
|
||||
request_deserializer=invoices__pb2.LookupInvoiceMsg.FromString,
|
||||
response_serializer=lightning__pb2.Invoice.SerializeToString,
|
||||
),
|
||||
'HtlcModifier': grpc.stream_stream_rpc_method_handler(
|
||||
servicer.HtlcModifier,
|
||||
request_deserializer=invoices__pb2.HtlcModifyResponse.FromString,
|
||||
response_serializer=invoices__pb2.HtlcModifyRequest.SerializeToString,
|
||||
),
|
||||
}
|
||||
generic_handler = grpc.method_handlers_generic_handler(
|
||||
'invoicesrpc.Invoices', rpc_method_handlers)
|
||||
server.add_generic_rpc_handlers((generic_handler,))
|
||||
server.add_registered_method_handlers('invoicesrpc.Invoices', rpc_method_handlers)
|
||||
|
||||
|
||||
# This class is part of an EXPERIMENTAL API.
|
||||
class Invoices(object):
|
||||
"""
|
||||
Comments in this file will be directly parsed into the API
|
||||
Documentation as descriptions of the associated method, message, or field.
|
||||
These descriptions should go right above the definition of the object, and
|
||||
can be in either block or // comment format.
|
||||
|
||||
An RPC method can be matched to an lncli command by placing a line in the
|
||||
beginning of the description in exactly the following format:
|
||||
lncli: `methodname`
|
||||
|
||||
Failure to specify the exact name of the command will cause documentation
|
||||
generation to fail.
|
||||
|
||||
More information on how exactly the gRPC documentation is generated from
|
||||
this proto file can be found here:
|
||||
https://github.com/lightninglabs/lightning-api
|
||||
|
||||
Invoices is a service that can be used to create, accept, settle and cancel
|
||||
invoices.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def SubscribeSingleInvoice(request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.unary_stream(
|
||||
request,
|
||||
target,
|
||||
'/invoicesrpc.Invoices/SubscribeSingleInvoice',
|
||||
invoices__pb2.SubscribeSingleInvoiceRequest.SerializeToString,
|
||||
lightning__pb2.Invoice.FromString,
|
||||
options,
|
||||
channel_credentials,
|
||||
insecure,
|
||||
call_credentials,
|
||||
compression,
|
||||
wait_for_ready,
|
||||
timeout,
|
||||
metadata,
|
||||
_registered_method=True)
|
||||
|
||||
@staticmethod
|
||||
def CancelInvoice(request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.unary_unary(
|
||||
request,
|
||||
target,
|
||||
'/invoicesrpc.Invoices/CancelInvoice',
|
||||
invoices__pb2.CancelInvoiceMsg.SerializeToString,
|
||||
invoices__pb2.CancelInvoiceResp.FromString,
|
||||
options,
|
||||
channel_credentials,
|
||||
insecure,
|
||||
call_credentials,
|
||||
compression,
|
||||
wait_for_ready,
|
||||
timeout,
|
||||
metadata,
|
||||
_registered_method=True)
|
||||
|
||||
@staticmethod
|
||||
def AddHoldInvoice(request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.unary_unary(
|
||||
request,
|
||||
target,
|
||||
'/invoicesrpc.Invoices/AddHoldInvoice',
|
||||
invoices__pb2.AddHoldInvoiceRequest.SerializeToString,
|
||||
invoices__pb2.AddHoldInvoiceResp.FromString,
|
||||
options,
|
||||
channel_credentials,
|
||||
insecure,
|
||||
call_credentials,
|
||||
compression,
|
||||
wait_for_ready,
|
||||
timeout,
|
||||
metadata,
|
||||
_registered_method=True)
|
||||
|
||||
@staticmethod
|
||||
def SettleInvoice(request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.unary_unary(
|
||||
request,
|
||||
target,
|
||||
'/invoicesrpc.Invoices/SettleInvoice',
|
||||
invoices__pb2.SettleInvoiceMsg.SerializeToString,
|
||||
invoices__pb2.SettleInvoiceResp.FromString,
|
||||
options,
|
||||
channel_credentials,
|
||||
insecure,
|
||||
call_credentials,
|
||||
compression,
|
||||
wait_for_ready,
|
||||
timeout,
|
||||
metadata,
|
||||
_registered_method=True)
|
||||
|
||||
@staticmethod
|
||||
def LookupInvoiceV2(request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.unary_unary(
|
||||
request,
|
||||
target,
|
||||
'/invoicesrpc.Invoices/LookupInvoiceV2',
|
||||
invoices__pb2.LookupInvoiceMsg.SerializeToString,
|
||||
lightning__pb2.Invoice.FromString,
|
||||
options,
|
||||
channel_credentials,
|
||||
insecure,
|
||||
call_credentials,
|
||||
compression,
|
||||
wait_for_ready,
|
||||
timeout,
|
||||
metadata,
|
||||
_registered_method=True)
|
||||
|
||||
@staticmethod
|
||||
def HtlcModifier(request_iterator,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.stream_stream(
|
||||
request_iterator,
|
||||
target,
|
||||
'/invoicesrpc.Invoices/HtlcModifier',
|
||||
invoices__pb2.HtlcModifyResponse.SerializeToString,
|
||||
invoices__pb2.HtlcModifyRequest.FromString,
|
||||
options,
|
||||
channel_credentials,
|
||||
insecure,
|
||||
call_credentials,
|
||||
compression,
|
||||
wait_for_ready,
|
||||
timeout,
|
||||
metadata,
|
||||
_registered_method=True)
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load diff
|
|
@ -8,13 +8,15 @@ from typing import Optional
|
|||
import grpc
|
||||
from loguru import logger
|
||||
|
||||
import lnbits.wallets.lnd_grpc_files.invoices_pb2 as invoices
|
||||
import lnbits.wallets.lnd_grpc_files.invoices_pb2_grpc as invoicesrpc
|
||||
import lnbits.wallets.lnd_grpc_files.lightning_pb2 as ln
|
||||
import lnbits.wallets.lnd_grpc_files.lightning_pb2_grpc as lnrpc
|
||||
import lnbits.wallets.lnd_grpc_files.router_pb2 as router
|
||||
import lnbits.wallets.lnd_grpc_files.router_pb2_grpc as routerrpc
|
||||
from lnbits.helpers import normalize_endpoint
|
||||
from lnbits.settings import settings
|
||||
from lnbits.utils.crypto import random_secret_and_hash
|
||||
from lnbits.wallets.lnd_grpc_files.router_pb2_grpc import RouterStub
|
||||
|
||||
from .base import (
|
||||
InvoiceResponse,
|
||||
|
|
@ -98,7 +100,8 @@ class LndWallet(Wallet):
|
|||
f"{self.endpoint}:{self.port}", composite_creds
|
||||
)
|
||||
self.rpc = lnrpc.LightningStub(channel)
|
||||
self.routerpc = routerrpc.RouterStub(channel)
|
||||
self.routerpc = RouterStub(channel)
|
||||
self.invoicesrpc = invoicesrpc.InvoicesStub(channel)
|
||||
|
||||
def metadata_callback(self, _, callback):
|
||||
callback([("macaroon", self.macaroon)], None)
|
||||
|
|
@ -108,7 +111,7 @@ class LndWallet(Wallet):
|
|||
|
||||
async def status(self) -> StatusResponse:
|
||||
try:
|
||||
resp = await self.rpc.ChannelBalance(ln.ChannelBalanceRequest())
|
||||
resp = await self.rpc.ChannelBalance(ln.ChannelBalanceRequest()) # type: ignore
|
||||
except Exception as exc:
|
||||
return StatusResponse(f"Unable to connect, got: '{exc}'", 0)
|
||||
|
||||
|
|
@ -144,7 +147,7 @@ class LndWallet(Wallet):
|
|||
data["r_hash"] = bytes.fromhex(payment_hash)
|
||||
data["r_preimage"] = bytes.fromhex(preimage)
|
||||
try:
|
||||
req = ln.Invoice(**data)
|
||||
req = ln.Invoice(**data) # type: ignore
|
||||
resp = await self.rpc.AddInvoice(req)
|
||||
# response model
|
||||
# {
|
||||
|
|
@ -168,7 +171,7 @@ class LndWallet(Wallet):
|
|||
|
||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||
# fee_limit_fixed = ln.FeeLimit(fixed=fee_limit_msat // 1000)
|
||||
req = router.SendPaymentRequest(
|
||||
req = router.SendPaymentRequest( # type: ignore
|
||||
payment_request=bolt11,
|
||||
fee_limit_msat=fee_limit_msat,
|
||||
timeout_seconds=30,
|
||||
|
|
@ -227,7 +230,7 @@ class LndWallet(Wallet):
|
|||
# that use different checking_id formats
|
||||
raise ValueError
|
||||
|
||||
resp = await self.rpc.LookupInvoice(ln.PaymentHash(r_hash=r_hash))
|
||||
resp = await self.rpc.LookupInvoice(ln.PaymentHash(r_hash=r_hash)) # type: ignore
|
||||
if resp.settled:
|
||||
return PaymentSuccessStatus(preimage=resp.r_preimage.hex())
|
||||
|
||||
|
|
@ -271,7 +274,7 @@ class LndWallet(Wallet):
|
|||
|
||||
try:
|
||||
resp = self.routerpc.TrackPaymentV2(
|
||||
router.TrackPaymentRequest(payment_hash=r_hash)
|
||||
router.TrackPaymentRequest(payment_hash=r_hash) # type: ignore
|
||||
)
|
||||
async for payment in resp:
|
||||
if len(payment.htlcs) and statuses[payment.status]:
|
||||
|
|
@ -288,7 +291,7 @@ class LndWallet(Wallet):
|
|||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
while settings.lnbits_running:
|
||||
try:
|
||||
request = ln.InvoiceSubscription()
|
||||
request = ln.InvoiceSubscription() # type: ignore
|
||||
async for i in self.rpc.SubscribeInvoices(request):
|
||||
if not i.settled:
|
||||
continue
|
||||
|
|
@ -301,3 +304,65 @@ class LndWallet(Wallet):
|
|||
"retrying in 5 seconds"
|
||||
)
|
||||
await asyncio.sleep(5)
|
||||
|
||||
async def create_hold_invoice(
|
||||
self,
|
||||
amount: int,
|
||||
payment_hash: str,
|
||||
memo: Optional[str] = None,
|
||||
description_hash: Optional[bytes] = None,
|
||||
unhashed_description: Optional[bytes] = None,
|
||||
**kwargs,
|
||||
) -> InvoiceResponse:
|
||||
data: dict = {
|
||||
"description_hash": b"",
|
||||
"value": amount,
|
||||
"hash": hex_to_bytes(payment_hash),
|
||||
"private": True,
|
||||
"memo": memo or "",
|
||||
}
|
||||
if kwargs.get("expiry"):
|
||||
data["expiry"] = kwargs["expiry"]
|
||||
if description_hash:
|
||||
data["description_hash"] = description_hash
|
||||
elif unhashed_description:
|
||||
data["description_hash"] = sha256(unhashed_description).digest()
|
||||
try:
|
||||
req = invoices.AddHoldInvoiceRequest(**data) # type: ignore
|
||||
res = await self.invoicesrpc.AddHoldInvoice(req)
|
||||
logger.debug(f"AddHoldInvoice response: {res}")
|
||||
except Exception as exc:
|
||||
logger.warning(exc)
|
||||
error_message = str(exc)
|
||||
return InvoiceResponse(ok=False, error_message=error_message)
|
||||
return InvoiceResponse(
|
||||
ok=True, checking_id=payment_hash, payment_request=str(res.payment_request)
|
||||
)
|
||||
|
||||
async def settle_hold_invoice(self, preimage: str) -> InvoiceResponse:
|
||||
try:
|
||||
req = invoices.SettleInvoiceMsg(preimage=hex_to_bytes(preimage)) # type: ignore
|
||||
await self.invoicesrpc.SettleInvoice(req)
|
||||
except grpc.aio.AioRpcError as exc:
|
||||
return InvoiceResponse(
|
||||
ok=False, error_message=exc.details() or "unknown grpc exception"
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning(exc)
|
||||
return InvoiceResponse(ok=False, error_message=str(exc))
|
||||
return InvoiceResponse(ok=True, preimage=preimage)
|
||||
|
||||
async def cancel_hold_invoice(self, payment_hash: str) -> InvoiceResponse:
|
||||
try:
|
||||
req = invoices.CancelInvoiceMsg(payment_hash=hex_to_bytes(payment_hash)) # type: ignore
|
||||
res = await self.invoicesrpc.CancelInvoice(req)
|
||||
logger.debug(f"CancelInvoice response: {res}")
|
||||
except Exception as exc:
|
||||
logger.warning(exc)
|
||||
# If we cannot cancel the invoice, we return an error message
|
||||
# and True for ok that should be ignored by the service
|
||||
return InvoiceResponse(
|
||||
ok=False, checking_id=payment_hash, error_message=str(exc)
|
||||
)
|
||||
# If we reach here, the invoice was successfully canceled and payment failed
|
||||
return InvoiceResponse(True, checking_id=payment_hash)
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ from .macaroon import load_macaroon
|
|||
|
||||
|
||||
class LndRestWallet(Wallet):
|
||||
"""https://api.lightning.community/rest/index.html#lnd-rest-api-reference"""
|
||||
"""https://api.lightning.community/#lnd-rest-api-reference"""
|
||||
|
||||
__node_cls__ = LndRestNode
|
||||
features = [Feature.nodemanager]
|
||||
|
|
@ -322,3 +322,77 @@ class LndRestWallet(Wallet):
|
|||
" seconds"
|
||||
)
|
||||
await asyncio.sleep(5)
|
||||
|
||||
async def create_hold_invoice(
|
||||
self,
|
||||
amount: int,
|
||||
payment_hash: str,
|
||||
memo: Optional[str] = None,
|
||||
description_hash: Optional[bytes] = None,
|
||||
unhashed_description: Optional[bytes] = None,
|
||||
**_,
|
||||
) -> InvoiceResponse:
|
||||
data: dict = {
|
||||
"value": amount,
|
||||
"private": True,
|
||||
"hash": base64.b64encode(bytes.fromhex(payment_hash)).decode("ascii"),
|
||||
}
|
||||
if description_hash:
|
||||
data["description_hash"] = base64.b64encode(description_hash).decode(
|
||||
"ascii"
|
||||
)
|
||||
elif unhashed_description:
|
||||
data["description_hash"] = base64.b64encode(
|
||||
hashlib.sha256(unhashed_description).digest()
|
||||
).decode("ascii")
|
||||
else:
|
||||
data["memo"] = memo or ""
|
||||
|
||||
try:
|
||||
r = await self.client.post(url="/v2/invoices/hodl", json=data)
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
except httpx.HTTPStatusError as exc:
|
||||
logger.warning(exc)
|
||||
return InvoiceResponse(ok=False, error_message=exc.response.text)
|
||||
except Exception as exc:
|
||||
logger.error(exc)
|
||||
return InvoiceResponse(ok=False, error_message=str(exc))
|
||||
|
||||
payment_request = data["payment_request"]
|
||||
payment_hash = base64.b64encode(bytes.fromhex(payment_hash)).decode("ascii")
|
||||
|
||||
return InvoiceResponse(
|
||||
ok=True, checking_id=payment_hash, payment_request=payment_request
|
||||
)
|
||||
|
||||
async def settle_hold_invoice(self, preimage: str) -> InvoiceResponse:
|
||||
data: dict = {
|
||||
"preimage": base64.b64encode(bytes.fromhex(preimage)).decode("ascii")
|
||||
}
|
||||
try:
|
||||
r = await self.client.post(url="/v2/invoices/settle", json=data)
|
||||
r.raise_for_status()
|
||||
return InvoiceResponse(ok=True, preimage=preimage)
|
||||
except httpx.HTTPStatusError as exc:
|
||||
logger.warning(exc)
|
||||
return InvoiceResponse(ok=False, error_message=exc.response.text)
|
||||
except Exception as exc:
|
||||
logger.error(exc)
|
||||
return InvoiceResponse(ok=False, error_message=str(exc))
|
||||
|
||||
async def cancel_hold_invoice(self, payment_hash: str) -> InvoiceResponse:
|
||||
rhash = bytes.fromhex(payment_hash)
|
||||
try:
|
||||
r = await self.client.post(
|
||||
url="/v2/invoices/cancel",
|
||||
json={"payment_hash": base64.b64encode(rhash).decode("ascii")},
|
||||
)
|
||||
r.raise_for_status()
|
||||
logger.debug(f"Cancel hold invoice response: {r.text}")
|
||||
return InvoiceResponse(ok=True, checking_id=payment_hash)
|
||||
except httpx.HTTPStatusError as exc:
|
||||
return InvoiceResponse(ok=False, error_message=exc.response.text)
|
||||
except Exception as exc:
|
||||
logger.error(exc)
|
||||
return InvoiceResponse(ok=False, error_message=str(exc))
|
||||
|
|
|
|||
118
tests/regtest/test_real_hold_invoice.py
Normal file
118
tests/regtest/test_real_hold_invoice.py
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
import asyncio
|
||||
|
||||
import pytest
|
||||
|
||||
from lnbits.core.models import Payment
|
||||
from lnbits.core.services.payments import (
|
||||
cancel_hold_invoice,
|
||||
create_invoice,
|
||||
get_standalone_payment,
|
||||
settle_hold_invoice,
|
||||
)
|
||||
from lnbits.exceptions import InvoiceError
|
||||
from lnbits.utils.crypto import random_secret_and_hash
|
||||
|
||||
from ..helpers import funding_source, is_fake
|
||||
from .helpers import pay_real_invoice
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.skipif(is_fake, reason="this only works in regtest")
|
||||
@pytest.mark.skipif(
|
||||
funding_source.__class__.__name__ in ["LndRestWallet", "LndWallet"],
|
||||
reason="this should not raise for lnd",
|
||||
)
|
||||
async def test_pay_raise_unsupported(app):
|
||||
payment_hash = "0" * 32
|
||||
payment = Payment(
|
||||
checking_id=payment_hash,
|
||||
amount=1000,
|
||||
wallet_id="fake_wallet_id",
|
||||
bolt11="fake_holdinvoice",
|
||||
payment_hash=payment_hash,
|
||||
fee=0,
|
||||
)
|
||||
with pytest.raises(InvoiceError):
|
||||
await create_invoice(
|
||||
wallet_id="fake_wallet_id",
|
||||
amount=1000,
|
||||
memo="fake_holdinvoice",
|
||||
payment_hash=payment_hash,
|
||||
)
|
||||
with pytest.raises(InvoiceError):
|
||||
await settle_hold_invoice(payment, payment_hash)
|
||||
with pytest.raises(InvoiceError):
|
||||
await cancel_hold_invoice(payment)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.skipif(is_fake, reason="this only works in regtest")
|
||||
@pytest.mark.skipif(
|
||||
funding_source.__class__.__name__ not in ["LndRestWallet", "LndWallet"],
|
||||
reason="this only works for lndrest",
|
||||
)
|
||||
async def test_cancel_real_hold_invoice(app, from_wallet):
|
||||
|
||||
_, payment_hash = random_secret_and_hash()
|
||||
payment = await create_invoice(
|
||||
wallet_id=from_wallet.id,
|
||||
amount=1000,
|
||||
memo="test_cancel_holdinvoice",
|
||||
payment_hash=payment_hash,
|
||||
)
|
||||
assert payment.amount == 1000 * 1000
|
||||
assert payment.memo == "test_cancel_holdinvoice"
|
||||
assert payment.status == "pending"
|
||||
assert payment.wallet_id == from_wallet.id
|
||||
|
||||
payment = await cancel_hold_invoice(payment=payment)
|
||||
assert payment.ok is True
|
||||
|
||||
updated_payment = await get_standalone_payment(payment_hash, incoming=True)
|
||||
|
||||
assert updated_payment
|
||||
assert updated_payment.status == "failed"
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.skipif(is_fake, reason="this only works in regtest")
|
||||
@pytest.mark.skipif(
|
||||
funding_source.__class__.__name__ not in ["LndRestWallet", "LndWallet"],
|
||||
reason="this only works for lndrest",
|
||||
)
|
||||
async def test_settle_real_hold_invoice(app, from_wallet):
|
||||
|
||||
preimage, payment_hash = random_secret_and_hash()
|
||||
payment = await create_invoice(
|
||||
wallet_id=from_wallet.id,
|
||||
amount=1000,
|
||||
memo="test_settle_holdinvoice",
|
||||
payment_hash=payment_hash,
|
||||
)
|
||||
assert payment.amount == 1000 * 1000
|
||||
assert payment.memo == "test_settle_holdinvoice"
|
||||
assert payment.status == "pending"
|
||||
assert payment.wallet_id == from_wallet.id
|
||||
|
||||
# invoice should still be open
|
||||
with pytest.raises(InvoiceError):
|
||||
await settle_hold_invoice(payment=payment, preimage=preimage)
|
||||
|
||||
def pay_invoice():
|
||||
pay_real_invoice(payment.bolt11)
|
||||
|
||||
async def settle():
|
||||
await asyncio.sleep(1)
|
||||
invoice_response = await settle_hold_invoice(payment=payment, preimage=preimage)
|
||||
assert invoice_response.ok is True
|
||||
|
||||
coro = asyncio.to_thread(pay_invoice)
|
||||
task = asyncio.create_task(coro)
|
||||
await asyncio.gather(task, settle())
|
||||
|
||||
await asyncio.sleep(1)
|
||||
|
||||
updated_payment = await get_standalone_payment(payment_hash, incoming=True)
|
||||
|
||||
assert updated_payment
|
||||
assert updated_payment.status == "success"
|
||||
Loading…
Add table
Add a link
Reference in a new issue