- Introduced comprehensive documentation for the new Wallet Module, detailing its purpose, features, and key components. - Added WebSocket integration documentation, outlining real-time balance updates, connection management, and error handling. - Updated README and module index files to include references to the Wallet Module, enhancing overall module visibility and accessibility. These changes improve the clarity and usability of the Wallet Module, providing developers with essential information for implementation and integration.
15 KiB
💳 Payment Service
Centralized Lightning payment processing with wallet management, QR code generation, and real-time balance tracking integrated with LNbits.
Table of Contents
- #Overview
- #Architecture
- #Payment Processing
- #Wallet Management
- #Balance Management
- #QR Code Generation
- #Error Handling
- #Integration with WebSocket
- #API Reference
Overview
The PaymentService is a core infrastructure service that centralizes all Lightning Network payment operations in the Ario application. It provides a clean abstraction layer over LNbits operations and manages wallet state across all modules.
Key Responsibilities
- Payment Processing - Handle Lightning invoices, external wallets, and payment validation
- Wallet Management - Multi-wallet support with consistent wallet selection
- Balance Tracking - Real-time balance updates from WebSocket integration
- QR Code Generation - Payment request and custom data QR codes
- Error Handling - Consistent payment error handling and user feedback
Service Integration
┌─ UI Components ─┐ ┌─ PaymentService ─┐ ┌─ External Services ─┐
│ │ │ │ │ │
│ • Send Dialog │───▶│ • Payment Logic │───▶│ • LNbits API │
│ • Balance Display│ │ • Wallet Mgmt │ │ • External Wallets │
│ • QR Generator │ │ • Balance State │ │ • QR Library │
│ │ │ • Error Handling │ │ │
└─────────────────┘ └──────────────────┘ └─────────────────────┘
│
▼
┌─ AuthService ─────┐
│ │
│ • User Wallets │
│ • Authentication │
│ • Wallet Keys │
└───────────────────┘
Architecture
Service Structure
export class PaymentService extends BaseService {
// Service metadata
protected readonly metadata = {
name: 'PaymentService',
version: '1.0.0',
dependencies: ['AuthService']
}
// Reactive state
private _isProcessingPayment = ref(false)
private _paymentError = ref<string | null>(null)
// Public computed state
public readonly isProcessingPayment = computed(() => this._isProcessingPayment.value)
public readonly paymentError = computed(() => this._paymentError.value)
}
Dependencies
- AuthService - User authentication and wallet access
- BaseService - Service lifecycle and error handling
- Vue Reactivity - Reactive state management
External Integrations
- LNbits API - Lightning wallet backend
- QRCode Library - QR code generation
- External Wallets - Lightning wallet applications
- Browser APIs - Clipboard, window.open for external wallets
Payment Processing
Payment Types Supported
Lightning Invoices (BOLT11)
Process Lightning payment requests directly:
await paymentService.payWithWallet(
'lnbc1000n1p3xh4v...', // Lightning invoice
undefined, // Amount already in invoice
{
successMessage: 'Payment sent successfully!',
showToast: true
}
)
External Wallet Fallback
Automatically fall back to external wallets when user wallet lacks balance:
// Automatic payment handling with fallback
const result = await paymentService.handlePayment(
paymentRequest,
requiredAmount,
{ successMessage: 'Payment completed!' }
)
// Returns PaymentResult for wallet payments, null for external wallet
if (result) {
console.log('Paid with user wallet:', result.payment_hash)
} else {
console.log('Opened external wallet')
}
Payment Flow
1. Payment Request Validation
├── Check payment request format
├── Extract amount if present
└── Validate against available balance
2. Wallet Selection
├── Get preferred wallet
├── Check sufficient balance
└── Retrieve wallet credentials
3. Payment Execution
├── Call LNbits payment API
├── Handle response/errors
└── Update UI state
4. Completion Handling
├── Show success/error notifications
├── Reset payment state
└── Emit completion events
Payment Options
interface PaymentOptions {
successMessage?: string // Custom success message
errorMessage?: string // Custom error message
showToast?: boolean // Show toast notifications (default: true)
}
Wallet Management
Multi-Wallet Support
PaymentService provides consistent wallet selection across all modules:
// Preferred wallet selection (always first wallet)
const wallet = paymentService.getPreferredWallet()
// Returns: wallets[0] or null if no wallets
// Wallet with sufficient balance
const wallet = paymentService.getWalletWithBalance(1000) // 1000 sats
// Returns: first wallet with >= 1000 sats or null
// Wallet credentials access
const adminKey = paymentService.getPreferredWalletAdminKey()
const invoiceKey = paymentService.getPreferredWalletInvoiceKey()
Wallet Selection Logic
The service follows a consistent wallet selection strategy:
- Preferred Wallet: Always use
wallets[0]for consistency - Balance Checking: Check if preferred wallet has sufficient balance
- Fallback Strategy: No automatic fallback - use external wallet instead
- Credential Access: Provide admin/invoice keys for API operations
Balance Access
// Total balance across all wallets (in millisats)
const totalBalance = paymentService.totalBalance
// Check if any wallet has balance
const hasBalance = paymentService.hasWalletWithBalance
// Balance formatted for display
const balanceSats = Math.round(totalBalance / 1000)
Balance Management
Real-Time Balance Updates
PaymentService receives balance updates from WalletWebSocketService:
// Called by WebSocket service when balance changes
paymentService.updateWalletBalance(balanceMsat, walletId)
Balance Update Logic
updateWalletBalance(balanceMsat: number, walletId?: string): void {
// Determine target wallet
let walletToUpdate
if (walletId) {
walletToUpdate = this.authService.user.value.wallets.find(w => w.id === walletId)
} else {
walletToUpdate = this.getPreferredWallet() // Default to first wallet
}
if (walletToUpdate) {
const oldBalance = walletToUpdate.balance_msat
walletToUpdate.balance_msat = balanceMsat
// Trigger Vue reactivity
this.authService.user.value = { ...this.authService.user.value }
console.log(`Wallet ${walletToUpdate.id} balance: ${oldBalance} → ${balanceMsat} msat`)
}
}
Balance State Management
- Storage: Balances stored in AuthService user object
- Reactivity: Vue reactivity ensures UI updates automatically
- Consistency: Single source of truth for wallet balances
- Persistence: Balances persist across app sessions via AuthService
QR Code Generation
Payment Request QR Codes
Generate QR codes for Lightning payment requests:
// Standard payment QR code
const qrDataUrl = await paymentService.generateQRCode(
'lnbc1000n1p3xh4v...', // Payment request
{
width: 256, // QR code size
margin: 2, // Border margin
darkColor: '#000000', // QR code color
lightColor: '#FFFFFF' // Background color
}
)
// Use in components
<img :src="qrDataUrl" alt="Payment QR Code" />
Custom Data QR Codes
Generate QR codes for tickets, links, or other data:
// Ticket QR code
const ticketQR = await paymentService.generateCustomQRCode(
JSON.stringify({
eventId: 'event123',
ticketId: 'ticket456',
timestamp: Date.now()
}),
{ width: 128 } // Smaller size for tickets
)
QR Code Options
interface QRCodeOptions {
width?: number // QR code width in pixels (default: 256)
margin?: number // Border margin (default: 2)
darkColor?: string // QR code color (default: #000000)
lightColor?: string // Background color (default: #FFFFFF)
}
Error Handling
Payment Error Types
// Common payment errors
enum PaymentErrorType {
INSUFFICIENT_BALANCE = 'Insufficient wallet balance',
INVALID_INVOICE = 'Invalid payment request',
NETWORK_ERROR = 'Network connection failed',
TIMEOUT = 'Payment timeout',
ROUTING_FAILED = 'Lightning routing failed',
WALLET_LOCKED = 'Wallet is locked',
UNKNOWN = 'Unknown payment error'
}
Error Handling Pattern
async payWithWallet(paymentRequest: string): Promise<PaymentResult> {
try {
this._isProcessingPayment.value = true
this._paymentError.value = null
// Payment processing logic
const result = await this.processPayment(paymentRequest)
// Success notification
toast.success('Payment successful!')
return result
} catch (error) {
// Error handling
const errorMessage = this.parsePaymentError(error)
this._paymentError.value = errorMessage
// User notification
toast.error('Payment failed', {
description: errorMessage
})
throw error
} finally {
this._isProcessingPayment.value = false
}
}
Error Recovery
// Clear error state
paymentService.clearPaymentError()
// Force reset payment state (for debugging)
paymentService.forceResetPaymentState()
// Retry payment after error
await paymentService.payWithWallet(paymentRequest, amount, {
errorMessage: 'Retry failed - please try again'
})
Integration with WebSocket
WebSocket Collaboration
PaymentService works closely with WalletWebSocketService:
┌─ WalletWebSocketService ─┐ ┌─ PaymentService ─┐
│ │ │ │
│ 1. Receive WS Message │ │ │
│ 2. Parse Balance Update │───▶│ 4. Update Balance│
│ 3. Convert Sats→MSats │ │ 5. Trigger UI │
│ │ │ │
└──────────────────────────┘ └──────────────────┘
Balance Update Flow
- WebSocket Message: WalletWebSocketService receives balance update
- Unit Conversion: Convert sats to millisats for internal consistency
- Balance Update: Call
PaymentService.updateWalletBalance() - State Update: PaymentService updates wallet balance in AuthService
- UI Reactivity: Vue reactivity updates all UI components
Payment Notifications
// WebSocket triggers payment notification
eventBus.on('payment:received', (data) => {
// PaymentService can listen and handle accordingly
paymentService.handlePaymentNotification(data)
})
API Reference
Public Methods
Payment Operations
// Process Lightning payment with user wallet
payWithWallet(
paymentRequest: string,
requiredAmountSats?: number,
options?: PaymentOptions
): Promise<PaymentResult>
// Handle payment with automatic fallback
handlePayment(
paymentRequest: string,
requiredAmountSats?: number,
options?: PaymentOptions
): Promise<PaymentResult | null>
// Open external Lightning wallet
openExternalWallet(paymentRequest: string): void
Wallet Management
// Get preferred wallet (first wallet)
getPreferredWallet(): Wallet | null
// Get wallet with sufficient balance
getWalletWithBalance(requiredAmountSats?: number): Wallet | null
// Get wallet credentials
getPreferredWalletAdminKey(): string | null
getPreferredWalletInvoiceKey(): string | null
// Update wallet balance (called by WebSocket)
updateWalletBalance(balanceMsat: number, walletId?: string): void
QR Code Generation
// Generate payment QR code
generateQRCode(
paymentRequest: string,
options?: QRCodeOptions
): Promise<string>
// Generate custom data QR code
generateCustomQRCode(
data: string,
options?: QRCodeOptions
): Promise<string>
State Management
// Clear payment error
clearPaymentError(): void
// Reset payment state
resetPaymentState(): void
// Force reset (for debugging)
forceResetPaymentState(): void
Reactive State
// Access reactive state
const paymentService = injectService(SERVICE_TOKENS.PAYMENT_SERVICE)
// Payment processing state
const isProcessing = paymentService.isProcessingPayment
const error = paymentService.paymentError
// Wallet information
const totalBalance = paymentService.totalBalance
const hasBalance = paymentService.hasWalletWithBalance
Integration Example
<script setup>
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
const paymentService = injectService(SERVICE_TOKENS.PAYMENT_SERVICE)
// Reactive state
const { isProcessingPayment, paymentError, totalBalance } = paymentService
// Payment method
const sendPayment = async (invoice: string) => {
try {
const result = await paymentService.payWithWallet(invoice, undefined, {
successMessage: 'Payment sent successfully!'
})
console.log('Payment completed:', result.payment_hash)
} catch (error) {
console.error('Payment failed:', error.message)
}
}
// Formatted balance for display
const balanceDisplay = computed(() => {
return `${Math.round(totalBalance / 1000)} sats`
})
</script>
<template>
<div class="payment-interface">
<div class="balance">Balance: {{ balanceDisplay }}</div>
<button
@click="sendPayment(invoice)"
:disabled="isProcessingPayment || !hasBalance"
>
{{ isProcessingPayment ? 'Processing...' : 'Send Payment' }}
</button>
<div v-if="paymentError" class="error">
{{ paymentError }}
</div>
</div>
</template>
See Also
Related Documentation
- ../02-modules/wallet-module/index - Wallet UI and WebSocket integration
- ../02-modules/wallet-module/websocket-integration - Real-time balance updates
- authentication - User wallets and credentials
API References
- ../05-api-reference/lnbits-api - Backend Lightning wallet API
- ../05-api-reference/lightning-payments - Payment protocol details
Development Guides
- ../04-development/testing - Testing payment functionality
- ../04-development/debugging - Payment troubleshooting
Tags: #payment #lightning #service #wallet #qr-code #lnbits Last Updated: 2025-09-18 Author: Development Team