Enables balance payments via invoice

Adds functionality for users to pay their Castle balance by generating and paying a Lightning invoice.
This includes:
- Adding API endpoints for invoice generation and payment recording.
- Updating the frontend to allow users to initiate the invoice payment process.
- Passing the wallet's `inkey` to the frontend for payment status checks.
This commit is contained in:
padreug 2025-10-22 16:48:13 +02:00
parent ef3e2d9e0d
commit 854164614f
3 changed files with 35 additions and 9 deletions

View file

@ -153,3 +153,15 @@ class CreateManualPaymentRequest(BaseModel):
amount: int amount: int
description: str description: str
class GeneratePaymentInvoice(BaseModel):
"""Generate payment invoice request"""
amount: int
class RecordPayment(BaseModel):
"""Record a payment"""
payment_hash: str

View file

@ -34,6 +34,7 @@ window.app = Vue.createApp({
amount: null, amount: null,
paymentRequest: null, paymentRequest: null,
paymentHash: null, paymentHash: null,
checkWalletKey: null,
loading: false loading: false
}, },
settingsDialog: { settingsDialog: {
@ -326,6 +327,7 @@ window.app = Vue.createApp({
// Show the payment request in the dialog // Show the payment request in the dialog
this.payDialog.paymentRequest = response.data.payment_request this.payDialog.paymentRequest = response.data.payment_request
this.payDialog.paymentHash = response.data.payment_hash this.payDialog.paymentHash = response.data.payment_hash
this.payDialog.checkWalletKey = response.data.check_wallet_key
this.$q.notify({ this.$q.notify({
type: 'positive', type: 'positive',
@ -334,21 +336,21 @@ window.app = Vue.createApp({
}) })
// Poll for payment completion // Poll for payment completion
this.pollForPayment(response.data.payment_hash) this.pollForPayment(response.data.payment_hash, response.data.check_wallet_key)
} catch (error) { } catch (error) {
LNbits.utils.notifyApiError(error) LNbits.utils.notifyApiError(error)
} finally { } finally {
this.payDialog.loading = false this.payDialog.loading = false
} }
}, },
async pollForPayment(paymentHash) { async pollForPayment(paymentHash, checkWalletKey) {
// Poll every 2 seconds for payment status // Poll every 2 seconds for payment status
const checkPayment = async () => { const checkPayment = async () => {
try { try {
const response = await LNbits.api.request( const response = await LNbits.api.request(
'GET', 'GET',
`/api/v1/payments/${paymentHash}`, `/api/v1/payments/${paymentHash}`,
this.g.user.wallets[0].inkey checkWalletKey
) )
if (response.data && response.data.paid) { if (response.data && response.data.paid) {
@ -415,6 +417,7 @@ window.app = Vue.createApp({
this.payDialog.amount = Math.abs(this.balance.balance) this.payDialog.amount = Math.abs(this.balance.balance)
this.payDialog.paymentRequest = null this.payDialog.paymentRequest = null
this.payDialog.paymentHash = null this.payDialog.paymentHash = null
this.payDialog.checkWalletKey = null
this.payDialog.show = true this.payDialog.show = true
}, },
async showReceivableDialog() { async showReceivableDialog() {

View file

@ -34,8 +34,10 @@ from .models import (
CreateEntryLine, CreateEntryLine,
CreateJournalEntry, CreateJournalEntry,
ExpenseEntry, ExpenseEntry,
GeneratePaymentInvoice,
JournalEntry, JournalEntry,
ReceivableEntry, ReceivableEntry,
RecordPayment,
RevenueEntry, RevenueEntry,
UserBalance, UserBalance,
UserWalletSettings, UserWalletSettings,
@ -446,13 +448,14 @@ async def api_get_all_balances(
@castle_api_router.post("/api/v1/generate-payment-invoice") @castle_api_router.post("/api/v1/generate-payment-invoice")
async def api_generate_payment_invoice( async def api_generate_payment_invoice(
amount: int, data: GeneratePaymentInvoice,
wallet: WalletTypeInfo = Depends(require_invoice_key), wallet: WalletTypeInfo = Depends(require_invoice_key),
) -> dict: ) -> dict:
""" """
Generate an invoice on the Castle wallet for user to pay their balance. Generate an invoice on the Castle wallet for user to pay their balance.
User can then pay this invoice to settle their debt. User can then pay this invoice to settle their debt.
""" """
from lnbits.core.crud.wallets import get_wallet
from lnbits.core.models import CreateInvoice from lnbits.core.models import CreateInvoice
from lnbits.core.services import create_payment_request from lnbits.core.services import create_payment_request
@ -462,7 +465,7 @@ async def api_generate_payment_invoice(
# Create invoice on castle wallet # Create invoice on castle wallet
invoice_data = CreateInvoice( invoice_data = CreateInvoice(
out=False, out=False,
amount=amount, amount=data.amount,
memo=f"Payment from user {wallet.wallet.user[:8]} to Castle", memo=f"Payment from user {wallet.wallet.user[:8]} to Castle",
unit="sat", unit="sat",
extra={"user_id": wallet.wallet.user, "type": "castle_payment"}, extra={"user_id": wallet.wallet.user, "type": "castle_payment"},
@ -470,17 +473,25 @@ async def api_generate_payment_invoice(
payment = await create_payment_request(castle_wallet_id, invoice_data) payment = await create_payment_request(castle_wallet_id, invoice_data)
# Get castle wallet to return its inkey for payment checking
castle_wallet = await get_wallet(castle_wallet_id)
if not castle_wallet:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Castle wallet not found"
)
return { return {
"payment_hash": payment.payment_hash, "payment_hash": payment.payment_hash,
"payment_request": payment.bolt11, "payment_request": payment.bolt11,
"amount": amount, "amount": data.amount,
"memo": invoice_data.memo, "memo": invoice_data.memo,
"check_wallet_key": castle_wallet.inkey, # Key to check payment status
} }
@castle_api_router.post("/api/v1/record-payment") @castle_api_router.post("/api/v1/record-payment")
async def api_record_payment( async def api_record_payment(
payment_hash: str, data: RecordPayment,
wallet: WalletTypeInfo = Depends(require_invoice_key), wallet: WalletTypeInfo = Depends(require_invoice_key),
) -> dict: ) -> dict:
""" """
@ -490,7 +501,7 @@ async def api_record_payment(
from lnbits.core.crud.payments import get_standalone_payment from lnbits.core.crud.payments import get_standalone_payment
# Get the payment details # Get the payment details
payment = await get_standalone_payment(payment_hash) payment = await get_standalone_payment(data.payment_hash)
if not payment: if not payment:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Payment not found" status_code=HTTPStatus.NOT_FOUND, detail="Payment not found"
@ -518,7 +529,7 @@ async def api_record_payment(
# This reduces what the user owes # This reduces what the user owes
entry_data = CreateJournalEntry( entry_data = CreateJournalEntry(
description=f"Lightning payment from user {wallet.wallet.user[:8]}", description=f"Lightning payment from user {wallet.wallet.user[:8]}",
reference=payment_hash, reference=data.payment_hash,
lines=[ lines=[
CreateEntryLine( CreateEntryLine(
account_id=lightning_account.id, account_id=lightning_account.id,