- 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.
590 lines
No EOL
17 KiB
Markdown
590 lines
No EOL
17 KiB
Markdown
# ⚡ 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]]
|
||
- [[#Architecture]]
|
||
- [[#Connection Management]]
|
||
- [[#Message Processing]]
|
||
- [[#Balance Update Logic]]
|
||
- [[#Error Handling]]
|
||
- [[#Battery Optimization]]
|
||
- [[#Configuration]]
|
||
- [[#Troubleshooting]]
|
||
|
||
## 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**
|
||
```typescript
|
||
// Triggered by authentication events
|
||
eventBus.on('auth:login', () => {
|
||
setTimeout(() => {
|
||
this.connectIfNeeded()
|
||
}, 500) // Small delay for auth processing
|
||
})
|
||
```
|
||
|
||
#### **Connection Process**
|
||
```typescript
|
||
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**
|
||
```typescript
|
||
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**
|
||
```typescript
|
||
// Connection states tracked
|
||
enum ConnectionStatus {
|
||
DISCONNECTED = 'disconnected',
|
||
CONNECTING = 'connecting',
|
||
CONNECTED = 'connected',
|
||
RECONNECTING = 'reconnecting',
|
||
ERROR = 'error',
|
||
FAILED = 'failed'
|
||
}
|
||
```
|
||
|
||
### **Graceful Disconnection**
|
||
```typescript
|
||
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:
|
||
```json
|
||
{
|
||
"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**
|
||
```typescript
|
||
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**
|
||
```typescript
|
||
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**
|
||
```typescript
|
||
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**
|
||
```typescript
|
||
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**
|
||
```typescript
|
||
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**
|
||
```typescript
|
||
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)**
|
||
```typescript
|
||
// 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**
|
||
```typescript
|
||
// Network connectivity issues - retry with backoff
|
||
if (!navigator.onLine) {
|
||
window.addEventListener('online', () => {
|
||
this.reconnectAttempts = 0
|
||
this.connectIfNeeded()
|
||
}, { once: true })
|
||
}
|
||
```
|
||
|
||
#### **Server Errors (Code 1011)**
|
||
```typescript
|
||
// 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:
|
||
|
||
```typescript
|
||
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**
|
||
```typescript
|
||
private async onPause(): Promise<void> {
|
||
console.log('WebSocket pausing - app not visible')
|
||
|
||
// Disconnect to save battery
|
||
this.disconnect()
|
||
}
|
||
```
|
||
|
||
### **Resume Behavior**
|
||
```typescript
|
||
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:
|
||
|
||
```typescript
|
||
// 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**
|
||
```bash
|
||
# 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**
|
||
```typescript
|
||
// 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:**
|
||
```bash
|
||
# 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:**
|
||
```typescript
|
||
// 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**
|
||
```vue
|
||
<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**
|
||
```javascript
|
||
// 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**
|
||
```typescript
|
||
// 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
|
||
|
||
### Related Documentation
|
||
- **[[index|💰 Wallet Module Overview]]** - Main wallet module documentation
|
||
- **[[../../03-core-services/visibility-service|👁️ Visibility Service]]** - Battery optimization service
|
||
- **[[../../03-core-services/payment-service|💳 Payment Service]]** - Core payment processing
|
||
|
||
### API References
|
||
- **[[../../05-api-reference/lnbits-api|⚡ LNbits API]]** - Backend API documentation
|
||
- **[[../../05-api-reference/websocket-protocol|🔌 WebSocket Protocol]]** - WebSocket message formats
|
||
|
||
### Development Guides
|
||
- **[[../../04-development/testing|🧪 Testing Guide]]** - Testing WebSocket functionality
|
||
- **[[../../04-development/debugging|🐛 Debugging]]** - Debugging WebSocket issues
|
||
|
||
---
|
||
|
||
**Tags:** #websocket #real-time #lightning #lnbits #battery-optimization #connection-management
|
||
**Last Updated:** 2025-09-18
|
||
**Author:** Development Team |