web-app/docs/03-core-services/payment-service.md
padreug 71cec00bfc Add Wallet Module documentation and WebSocket integration
- 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.
2025-09-18 09:56:19 +02:00

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

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:

  1. Preferred Wallet: Always use wallets[0] for consistency
  2. Balance Checking: Check if preferred wallet has sufficient balance
  3. Fallback Strategy: No automatic fallback - use external wallet instead
  4. 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

  1. WebSocket Message: WalletWebSocketService receives balance update
  2. Unit Conversion: Convert sats to millisats for internal consistency
  3. Balance Update: Call PaymentService.updateWalletBalance()
  4. State Update: PaymentService updates wallet balance in AuthService
  5. 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

API References

Development Guides


Tags: #payment #lightning #service #wallet #qr-code #lnbits Last Updated: 2025-09-18 Author: Development Team