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

View file

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