web-app/docs/02-modules/wallet-module/websocket-integration.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

17 KiB
Raw Blame History

WebSocket Integration

Real-time wallet balance updates through LNbits WebSocket API with automatic reconnection, battery optimization, and smart unit conversion handling.

Table of Contents

Overview

The Wallet WebSocket integration provides real-time synchronization between the Ario wallet interface and the LNbits Lightning backend. This enables instant balance updates and payment notifications without requiring page refreshes or manual polling.

Key Benefits

  • Instant UI Updates - Balance changes appear immediately when payments are sent or received
  • Payment Notifications - Toast notifications for incoming payments
  • Battery Efficient - Pauses when app is not visible to save mobile battery
  • Reliable Connection - Automatic reconnection with exponential backoff
  • Smart Conversion - Handles LNbits sats/millisats behavior differences

WebSocket Endpoint

wss://your-lnbits-server/api/v1/ws/{walletInkey}

Where walletInkey is the invoice/readonly key for the wallet.

Architecture

Service Integration

┌─ WalletWebSocketService ─┐    ┌─ PaymentService ─┐    ┌─ UI Components ─┐
│                          │    │                  │    │                  │
│ • WebSocket Connection   │───▶│ • Balance Update │───▶│ • Balance Display│
│ • Message Processing     │    │ • Wallet Mgmt    │    │ • Toast Notifications
│ • Reconnection Logic     │    │ • Multi-wallet   │    │ • Transaction List │
│ • Battery Optimization   │    │                  │    │                  │
└──────────────────────────┘    └──────────────────┘    └──────────────────┘
          │                               │
          │                               │
          ▼                               ▼
┌─ VisibilityService ─────┐    ┌─ AuthService ──────┐
│                         │    │                    │
│ • App Visibility        │    │ • User Wallets     │
│ • Connection Pausing    │    │ • Authentication   │
│ • Resume Management     │    │ • Wallet Selection │
└─────────────────────────┘    └────────────────────┘

Dependency Graph

WalletWebSocketService
├── BaseService (lifecycle management)
├── PaymentService (balance updates)
├── WalletService (transaction management)
├── AuthService (wallet credentials)
└── VisibilityService (battery optimization)

Initialization Flow

1. Module Installation
   ├── Create WalletWebSocketService instance
   ├── Register in DI container
   └── Initialize with dependencies

2. Service Initialization
   ├── Wait for dependencies (AuthService, PaymentService)
   ├── Register with VisibilityService
   ├── Set up auth event listeners
   └── Attempt initial connection

3. Authentication Events
   ├── auth:login → Connect WebSocket
   └── auth:logout → Disconnect WebSocket

4. Connection Lifecycle
   ├── Connect → Update status → Listen for messages
   ├── Message → Process → Update balance
   ├── Disconnect → Attempt reconnection
   └── Visibility Change → Pause/Resume

Connection Management

Connection Lifecycle

Initial Connection

// Triggered by authentication events
eventBus.on('auth:login', () => {
  setTimeout(() => {
    this.connectIfNeeded()
  }, 500) // Small delay for auth processing
})

Connection Process

private async connect(walletInkey: string): Promise<void> {
  // 1. Build WebSocket URL
  const baseUrl = import.meta.env.VITE_LNBITS_BASE_URL
  const wsProtocol = baseUrl.startsWith('https') ? 'wss:' : 'ws:'
  const host = baseUrl.replace(/^https?:\/\//, '').replace(/\/$/, '')
  const wsUrl = `${wsProtocol}//${host}/api/v1/ws/${walletInkey}`

  // 2. Create WebSocket connection
  this.ws = new WebSocket(wsUrl)

  // 3. Set up event handlers
  this.ws.onopen = this.handleOpen.bind(this)
  this.ws.onmessage = this.handleMessage.bind(this)
  this.ws.onclose = this.handleClose.bind(this)
  this.ws.onerror = this.handleWebSocketError.bind(this)
}

Reconnection Strategy

Exponential Backoff

private scheduleReconnect(): void {
  if (this.reconnectAttempts >= this.config.maxReconnectAttempts) {
    this.connectionStatus.value = 'failed'
    return
  }

  // Exponential backoff: 1s, 2s, 4s, 8s, 16s
  const delay = this.config.reconnectDelay * Math.pow(2, this.reconnectAttempts)
  this.reconnectAttempts++

  this.reconnectTimer = setTimeout(() => {
    this.connectIfNeeded()
  }, delay)
}

Connection Health Monitoring

// Connection states tracked
enum ConnectionStatus {
  DISCONNECTED = 'disconnected',
  CONNECTING = 'connecting',
  CONNECTED = 'connected',
  RECONNECTING = 'reconnecting',
  ERROR = 'error',
  FAILED = 'failed'
}

Graceful Disconnection

public disconnect(): void {
  // Clear reconnection timer
  if (this.reconnectTimer) {
    clearTimeout(this.reconnectTimer)
    this.reconnectTimer = null
  }

  // Close WebSocket with normal closure code
  if (this.ws) {
    this.ws.close(1000, 'Client disconnect')
    this.ws = null
  }

  // Update reactive state
  this.isConnected.value = false
  this.connectionStatus.value = 'disconnected'
}

Message Processing

Message Structure

LNbits WebSocket sends messages in this format:

{
  "wallet_balance": 1000820,  // Balance in sats
  "payment": {                // Payment details (optional)
    "amount": 100000,         // Amount in millisats (positive = received, negative = sent)
    "payment_hash": "abc123...",
    "description": "Payment description",
    "time": 1694123456,
    "pending": false
  }
}

Message Handler

private handleMessage(event: MessageEvent): void {
  try {
    const data = JSON.parse(event.data)

    // Process payment notification first
    if (data.payment) {
      this.handlePaymentNotification(data.payment)
    }

    // Process balance update
    if (data.wallet_balance !== undefined) {
      this.processBalanceUpdate(data)
    }
  } catch (error) {
    console.error('Failed to parse WebSocket message:', error)
  }
}

Payment Notification Processing

private handlePaymentNotification(payment: any): void {
  // Add to transaction history via WalletService
  if (this.walletService?.addTransaction) {
    this.walletService.addTransaction(payment)
  }

  // Show toast notification for incoming payments
  if (payment.amount > 0 && !payment.pending) {
    const amountSats = Math.abs(payment.amount / 1000)
    this.toast.success(`Received ${amountSats} sats!`)
  }
}

Balance Update Logic

LNbits WebSocket Behavior

LNbits has different behavior for incoming vs outgoing payments:

  • Incoming payments: WebSocket sends post-payment balance (already includes received amount)
  • Outgoing payments: WebSocket sends pre-payment balance (before deduction)

Smart Balance Conversion

private processBalanceUpdate(data: any): void {
  let finalBalance = data.wallet_balance // Balance in sats from LNbits

  if (data.payment) {
    if (data.payment.amount < 0) {
      // Outgoing payment - LNbits sends pre-payment balance
      // We need to subtract the payment amount
      const paymentSats = Math.abs(data.payment.amount) / 1000
      finalBalance = data.wallet_balance - paymentSats

      console.log('Outgoing payment adjustment:', {
        originalBalance: data.wallet_balance,
        paymentSats: paymentSats,
        finalBalance: finalBalance
      })
    } else if (data.payment.amount > 0) {
      // Incoming payment - LNbits sends post-payment balance
      // Use balance as-is
      console.log('Incoming payment - using balance as-is:', data.wallet_balance)
    }
  }

  // Update balance via PaymentService
  this.updateWalletBalance(finalBalance)
}

Balance Update Integration

private updateWalletBalance(balanceSats: number): void {
  // Convert sats to millisats for internal storage
  const balanceMsat = balanceSats * 1000

  // Get connected wallet ID
  const wallet = this.paymentService?.getPreferredWallet?.()
  const walletId = wallet?.id

  // Update via PaymentService for consistency
  if (this.paymentService?.updateWalletBalance) {
    this.paymentService.updateWalletBalance(balanceMsat, walletId)
  }
}

Unit Conversion Table

Source Unit Internal Storage Display
LNbits WebSocket sats millisats × 1000 sats ÷ 1000
LNbits API millisats millisats sats ÷ 1000
Payment amounts millisats millisats sats ÷ 1000
UI display sats millisats ÷ 1000 sats

Error Handling

Connection Errors

private handleWebSocketError(event: Event): void {
  console.error('WebSocket error:', event)
  this.connectionStatus.value = 'error'

  // Log additional error details
  if (this.ws) {
    console.error('WebSocket state:', this.ws.readyState)
    console.error('WebSocket URL:', this.ws.url)
  }
}

Connection Close Handling

private handleClose(event: CloseEvent): void {
  console.log('WebSocket closed:', event.code, event.reason)

  this.isConnected.value = false
  this.connectionStatus.value = 'disconnected'
  this.ws = null

  // Only reconnect if not a normal closure
  if (event.code !== 1000) {
    this.scheduleReconnect()
  }
}

Error Recovery Strategies

Authentication Errors (Code 1002)

// Invalid wallet credentials - clear and require re-authentication
if (event.code === 1002) {
  this.authService?.logout()
  this.toast.error('Wallet credentials invalid. Please login again.')
}

Network Errors

// Network connectivity issues - retry with backoff
if (!navigator.onLine) {
  window.addEventListener('online', () => {
    this.reconnectAttempts = 0
    this.connectIfNeeded()
  }, { once: true })
}

Server Errors (Code 1011)

// Server internal error - longer backoff
if (event.code === 1011) {
  this.config.reconnectDelay = 5000 // Increase delay
  this.scheduleReconnect()
}

Battery Optimization

VisibilityService Integration

Mobile browsers suspend WebSocket connections when the app loses visibility. The WebSocket service integrates with VisibilityService to handle this efficiently:

protected async onInitialize(): Promise<void> {
  // Register with VisibilityService
  if (this.visibilityService) {
    this.visibilityService.registerService(
      this.metadata.name,
      this.onResume.bind(this),
      this.onPause.bind(this)
    )
  }
}

Pause Behavior

private async onPause(): Promise<void> {
  console.log('WebSocket pausing - app not visible')

  // Disconnect to save battery
  this.disconnect()
}

Resume Behavior

private async onResume(): Promise<void> {
  console.log('WebSocket resuming - app visible')

  // Reconnect if not already connected
  if (!this.isConnected.value) {
    this.reconnectAttempts = 0 // Reset attempt counter
    await this.connectIfNeeded()
  }
}

Visibility States

App State WebSocket Action Battery Impact
Visible Connected Normal
Hidden/Background Disconnected Minimal
Tab Switch Disconnected Minimal
Resume Reconnect Brief spike

Configuration

WebSocket Configuration

Configure WebSocket behavior in module config:

// app.config.ts
modules: {
  wallet: {
    enabled: true,
    config: {
      websocket: {
        enabled: true,                    // Enable WebSocket functionality
        reconnectDelay: 1000,            // Initial reconnection delay (ms)
        maxReconnectAttempts: 5          // Maximum reconnection attempts
      }
    }
  }
}

Environment Variables

# LNbits server URL - must be accessible for WebSocket
VITE_LNBITS_BASE_URL=https://your-lnbits-server.com

# Development mode - enables verbose WebSocket logging
VITE_DEV_MODE=true

Runtime Configuration Access

// Access configuration in service
const appConfig = (window as any).appConfig
if (appConfig?.modules?.wallet?.config?.websocket) {
  this.config = { ...this.config, ...appConfig.modules.wallet.config.websocket }
}

Troubleshooting

Common Issues

WebSocket Connection Fails

Symptoms: Connection status shows 'error' or 'failed'

Debugging Steps:

  1. Check LNbits server accessibility
  2. Verify VITE_LNBITS_BASE_URL environment variable
  3. Check browser network tab for WebSocket connection attempts
  4. Verify wallet credentials (inkey) are valid

Solutions:

# Test LNbits server connectivity
curl https://your-lnbits-server.com/api/v1/wallet

# Check WebSocket endpoint manually
# Open browser console and test:
const ws = new WebSocket('wss://your-server/api/v1/ws/your-inkey')

Balance Not Updating

Symptoms: Payments are processed but UI balance doesn't change

Debugging Steps:

  1. Check browser console for WebSocket messages
  2. Verify PaymentService is receiving updateWalletBalance calls
  3. Check if wallet ID matches between services

Debug Logging:

// Enable debug logging in WalletWebSocketService
console.log('WebSocket message:', data)
console.log('Balance update:', { old: oldBalance, new: finalBalance })

Frequent Disconnections

Symptoms: WebSocket constantly reconnecting

Potential Causes:

  • Network instability
  • LNbits server restarts
  • Proxy/firewall interference
  • Mobile browser background throttling

Solutions:

  • Increase reconnectDelay in configuration
  • Check server logs for connection issues
  • Verify VisibilityService is working correctly

Diagnostic Tools

Connection Status Component

<template>
  <div class="websocket-status">
    <div :class="statusClass">
      {{ connectionStatus }}
    </div>
    <div v-if="!isConnected">
      <button @click="reconnect">Reconnect</button>
    </div>
  </div>
</template>

<script setup>
const websocketService = injectService(SERVICE_TOKENS.WALLET_WEBSOCKET_SERVICE)
const { isConnected, connectionStatus } = websocketService

const statusClass = computed(() => ({
  'status-connected': connectionStatus.value === 'connected',
  'status-connecting': connectionStatus.value === 'connecting',
  'status-error': connectionStatus.value === 'error'
}))

const reconnect = () => websocketService.reconnect()
</script>

Debug Console Commands

// Access services from browser console
const wsService = window.__DI_CONTAINER__.get('WALLET_WEBSOCKET_SERVICE')

// Check connection status
console.log('Connected:', wsService.isConnected.value)
console.log('Status:', wsService.connectionStatus.value)

// Manual reconnection
wsService.reconnect()

// View configuration
console.log('Config:', wsService.config)

Performance Monitoring

Connection Metrics

// Track connection performance
class ConnectionMetrics {
  private connectionAttempts = 0
  private successfulConnections = 0
  private totalDowntime = 0
  private lastConnectTime = 0

  onConnectionAttempt() {
    this.connectionAttempts++
    this.lastConnectTime = Date.now()
  }

  onConnectionSuccess() {
    this.successfulConnections++
    const connectTime = Date.now() - this.lastConnectTime
    console.log(`Connected in ${connectTime}ms`)
  }

  getSuccessRate(): number {
    return this.successfulConnections / this.connectionAttempts
  }
}

See Also

API References

Development Guides


Tags: #websocket #real-time #lightning #lnbits #battery-optimization #connection-management Last Updated: 2025-09-18 Author: Development Team