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.
This commit is contained in:
parent
832bf4d4ca
commit
71cec00bfc
6 changed files with 1748 additions and 1 deletions
|
|
@ -74,6 +74,34 @@ interface ModulePlugin {
|
|||
|
||||
**See:** [[base-module/index|📖 Base Module Documentation]]
|
||||
|
||||
### **Wallet Module** 💰
|
||||
**Purpose:** Lightning Network wallet management with real-time updates
|
||||
**Location:** `src/modules/wallet/`
|
||||
**Dependencies:** `['base']`
|
||||
|
||||
**Features:**
|
||||
- **Real-time Balance Updates** - WebSocket connection to LNbits for instant balance sync
|
||||
- **Payment Processing** - Send to Lightning invoices, addresses, and LNURL endpoints
|
||||
- **Transaction Management** - Comprehensive history with real-time transaction updates
|
||||
- **Payment Links** - Create LNURL payment links and Lightning addresses for receiving
|
||||
- **Multi-wallet Support** - Manage multiple wallets with automatic selection
|
||||
- **Battery Optimization** - Pauses WebSocket when app not visible to save battery
|
||||
|
||||
**Key Components:**
|
||||
- SendDialog for payment sending interface
|
||||
- ReceiveDialog for payment link creation
|
||||
- TransactionList with real-time updates
|
||||
- BalanceDisplay with WebSocket synchronization
|
||||
- PayLinkManager for LNURL link management
|
||||
|
||||
**WebSocket Integration:**
|
||||
- Real-time balance updates via LNbits WebSocket API
|
||||
- Smart unit conversion handling (sats/millisats)
|
||||
- Automatic reconnection with exponential backoff
|
||||
- VisibilityService integration for mobile battery optimization
|
||||
|
||||
**See:** [[wallet-module/index|📖 Wallet Module Documentation]]
|
||||
|
||||
### **Nostr Feed Module** 📰
|
||||
**Purpose:** Social feed and content discovery
|
||||
**Location:** `src/modules/nostr-feed/`
|
||||
|
|
@ -369,6 +397,7 @@ async dispose(): Promise<void> {
|
|||
|
||||
### Module Documentation
|
||||
- **[[base-module/index|🏗️ Base Module]]** - Core infrastructure services
|
||||
- **[[wallet-module/index|💰 Wallet Module]]** - Lightning wallet with real-time updates
|
||||
- **[[chat-module/index|💬 Chat Module]]** - Encrypted messaging system
|
||||
- **[[events-module/index|🎟️ Events Module]]** - Lightning event ticketing
|
||||
- **[[market-module/index|🛒 Market Module]]** - Nostr marketplace
|
||||
|
|
|
|||
512
docs/02-modules/wallet-module/index.md
Normal file
512
docs/02-modules/wallet-module/index.md
Normal file
|
|
@ -0,0 +1,512 @@
|
|||
# 💰 Wallet Module
|
||||
|
||||
> **Lightning Network wallet management** with real-time balance updates, payment processing, and comprehensive transaction management integrated with LNbits.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [[#Overview]]
|
||||
- [[#Core Features]]
|
||||
- [[#Architecture]]
|
||||
- [[#Real-Time WebSocket Integration]]
|
||||
- [[#Services]]
|
||||
- [[#Components]]
|
||||
- [[#Payment Processing]]
|
||||
- [[#Configuration]]
|
||||
- [[#Development Guide]]
|
||||
|
||||
## Overview
|
||||
|
||||
The Wallet Module provides comprehensive Lightning Network wallet functionality for the Ario application. It integrates with LNbits to handle payments, manage wallet balances, and provide real-time updates through WebSocket connections.
|
||||
|
||||
### **Key Capabilities**
|
||||
- **Real-time balance updates** via WebSocket connection to LNbits
|
||||
- **Payment processing** for Lightning invoices, Lightning addresses, and LNURL
|
||||
- **Transaction management** with comprehensive history and status tracking
|
||||
- **Pay link creation** for receiving payments with LNURL and Lightning addresses
|
||||
- **Multi-wallet support** with automatic wallet selection
|
||||
- **Battery optimization** through VisibilityService integration
|
||||
|
||||
### **Dependencies**
|
||||
- `['base']` - Requires base module for authentication and core services
|
||||
- **LNbits Server** - External Lightning wallet backend
|
||||
- **PaymentService** - Core payment processing service
|
||||
|
||||
## Core Features
|
||||
|
||||
### **🔄 Real-Time Balance Updates**
|
||||
Automatic wallet balance synchronization without page refreshes:
|
||||
|
||||
- **WebSocket Connection** - Direct connection to LNbits WebSocket API
|
||||
- **Instant Updates** - Balance changes reflect immediately in UI
|
||||
- **Smart Unit Conversion** - Handles LNbits sats/millisats conversion differences
|
||||
- **Payment Notifications** - Toast notifications for incoming payments
|
||||
- **Connection Management** - Automatic reconnection with exponential backoff
|
||||
|
||||
### **💸 Payment Processing**
|
||||
Comprehensive Lightning payment handling:
|
||||
|
||||
- **Lightning Invoices** - Pay BOLT11 invoices directly
|
||||
- **Lightning Addresses** - Send to Lightning addresses (user@domain.com)
|
||||
- **LNURL Pay** - Support for LNURL payment requests
|
||||
- **Payment Validation** - Pre-flight checks for sufficient balance
|
||||
- **Error Handling** - User-friendly error messages and recovery
|
||||
|
||||
### **📊 Transaction Management**
|
||||
Complete transaction history and tracking:
|
||||
|
||||
- **Transaction History** - Chronological list of all payments
|
||||
- **Real-time Updates** - New transactions appear instantly
|
||||
- **Transaction Details** - Amount, description, timestamp, fees, status
|
||||
- **Transaction Types** - Sent, received, pending, confirmed, failed
|
||||
- **Search and Filter** - Find specific transactions quickly
|
||||
|
||||
### **🔗 Payment Links (LNURL)**
|
||||
Create reusable payment endpoints:
|
||||
|
||||
- **LNURL Generation** - Create payment links for receiving
|
||||
- **Lightning Addresses** - Generate username@domain addresses
|
||||
- **Amount Ranges** - Set minimum and maximum payment amounts
|
||||
- **Comments Support** - Allow payment comments
|
||||
- **Link Management** - Create, view, and delete payment links
|
||||
|
||||
## Architecture
|
||||
|
||||
### **Service Layer**
|
||||
```
|
||||
src/modules/wallet/services/
|
||||
├── WalletService.ts # Core wallet operations
|
||||
├── WalletWebSocketService.ts # Real-time balance updates
|
||||
└── WalletAPI.ts # LNbits API integration
|
||||
```
|
||||
|
||||
### **Component Layer**
|
||||
```
|
||||
src/modules/wallet/components/
|
||||
├── SendDialog.vue # Payment sending interface
|
||||
├── ReceiveDialog.vue # Payment receiving interface
|
||||
├── TransactionList.vue # Transaction history display
|
||||
├── PayLinkManager.vue # LNURL payment link management
|
||||
└── BalanceDisplay.vue # Wallet balance component
|
||||
```
|
||||
|
||||
### **Views Layer**
|
||||
```
|
||||
src/modules/wallet/views/
|
||||
└── WalletPage.vue # Main wallet interface
|
||||
```
|
||||
|
||||
## Real-Time WebSocket Integration
|
||||
|
||||
### **Connection Architecture**
|
||||
The WebSocket service provides seamless real-time updates:
|
||||
|
||||
```typescript
|
||||
// WebSocket URL format
|
||||
wss://your-lnbits-server/api/v1/ws/{walletInkey}
|
||||
|
||||
// Connection lifecycle
|
||||
┌─ Authentication Event ──→ Connect WebSocket
|
||||
├─ Payment Received ─────→ Update Balance + Show Toast
|
||||
├─ Payment Sent ─────────→ Update Balance
|
||||
├─ Connection Lost ──────→ Automatic Reconnection
|
||||
└─ App Hidden ───────────→ Pause (Battery Optimization)
|
||||
```
|
||||
|
||||
### **Balance Update Logic**
|
||||
Handles LNbits WebSocket behavior differences:
|
||||
|
||||
```typescript
|
||||
// Incoming payments: LNbits sends post-payment balance
|
||||
if (payment.amount > 0) {
|
||||
// Use balance as-is (already includes received amount)
|
||||
updateBalance(websocket_balance)
|
||||
}
|
||||
|
||||
// Outgoing payments: LNbits sends pre-payment balance
|
||||
if (payment.amount < 0) {
|
||||
// Subtract payment amount from reported balance
|
||||
final_balance = websocket_balance - payment_amount_sats
|
||||
updateBalance(final_balance)
|
||||
}
|
||||
```
|
||||
|
||||
### **Connection Management**
|
||||
- **Auto-reconnection** with exponential backoff (1s, 2s, 4s, 8s, 16s)
|
||||
- **VisibilityService integration** for battery optimization
|
||||
- **Connection health monitoring** with status indicators
|
||||
- **Graceful disconnection** on logout
|
||||
|
||||
### **Error Handling**
|
||||
- Connection failures with retry logic
|
||||
- Invalid wallet credentials handling
|
||||
- Network interruption recovery
|
||||
- WebSocket protocol error handling
|
||||
|
||||
## Services
|
||||
|
||||
### **WalletService** 🏦
|
||||
**Purpose:** Core wallet operations and transaction management
|
||||
**Location:** `src/modules/wallet/services/WalletService.ts`
|
||||
**Token:** `SERVICE_TOKENS.WALLET_SERVICE`
|
||||
|
||||
**Key Features:**
|
||||
- Send Lightning payments (invoices, addresses, LNURL)
|
||||
- Create and manage LNURL payment links
|
||||
- Transaction history management
|
||||
- Payment link generation with Lightning addresses
|
||||
|
||||
**Reactive State:**
|
||||
```typescript
|
||||
interface WalletService {
|
||||
transactions: ComputedRef<PaymentTransaction[]> // Transaction history
|
||||
payLinks: ComputedRef<PayLink[]> // Created payment links
|
||||
isCreatingPayLink: ComputedRef<boolean> // Pay link creation state
|
||||
isSendingPayment: ComputedRef<boolean> // Payment sending state
|
||||
error: ComputedRef<string | null> // Error state
|
||||
}
|
||||
```
|
||||
|
||||
**Key Methods:**
|
||||
- `sendPayment(request: SendPaymentRequest)` - Send Lightning payment
|
||||
- `createReceiveAddress(params)` - Create LNURL payment link
|
||||
- `deletePayLink(linkId: string)` - Remove payment link
|
||||
- `refresh()` - Reload transactions and payment links
|
||||
|
||||
### **WalletWebSocketService** ⚡
|
||||
**Purpose:** Real-time wallet balance updates via WebSocket
|
||||
**Location:** `src/modules/wallet/services/WalletWebSocketService.ts`
|
||||
**Token:** `SERVICE_TOKENS.WALLET_WEBSOCKET_SERVICE`
|
||||
|
||||
**Key Features:**
|
||||
- WebSocket connection to LNbits
|
||||
- Real-time balance updates
|
||||
- Payment notification handling
|
||||
- Connection health management
|
||||
- Battery optimization via VisibilityService
|
||||
|
||||
**Reactive State:**
|
||||
```typescript
|
||||
interface WalletWebSocketService {
|
||||
isConnected: Ref<boolean> // Connection status
|
||||
connectionStatus: Ref<string> // Connection state
|
||||
}
|
||||
```
|
||||
|
||||
**Connection States:**
|
||||
- `disconnected` - Not connected
|
||||
- `connecting` - Connection in progress
|
||||
- `connected` - Successfully connected
|
||||
- `reconnecting` - Attempting to reconnect
|
||||
- `error` - Connection failed
|
||||
- `failed` - Max reconnection attempts reached
|
||||
|
||||
**Key Methods:**
|
||||
- `reconnect()` - Manual reconnection trigger
|
||||
- `disconnect()` - Graceful disconnection
|
||||
- `cleanup()` - Service disposal
|
||||
|
||||
### **PaymentService Integration** 💳
|
||||
The wallet module integrates with the core PaymentService:
|
||||
|
||||
**Shared Functionality:**
|
||||
- Wallet balance management via `PaymentService.updateWalletBalance()`
|
||||
- Preferred wallet selection with `PaymentService.getPreferredWallet()`
|
||||
- Admin/invoice key access for API operations
|
||||
- Multi-wallet support with consistent selection logic
|
||||
|
||||
## Components
|
||||
|
||||
### **SendDialog.vue** 📤
|
||||
Payment sending interface with comprehensive validation:
|
||||
|
||||
**Features:**
|
||||
- Support for Lightning invoices, addresses, and LNURL
|
||||
- Amount validation against available balance
|
||||
- Payment request parsing and validation
|
||||
- Real-time fee estimation
|
||||
- Error handling with user-friendly messages
|
||||
|
||||
**Form Fields:**
|
||||
- Destination (invoice, address, or LNURL)
|
||||
- Amount (when not specified in invoice)
|
||||
- Comment (for Lightning addresses)
|
||||
|
||||
### **ReceiveDialog.vue** 📥
|
||||
Payment receiving interface for creating payment requests:
|
||||
|
||||
**Features:**
|
||||
- LNURL payment link creation
|
||||
- Lightning address generation
|
||||
- Customizable amount ranges
|
||||
- Comment support configuration
|
||||
- QR code generation for sharing
|
||||
|
||||
**Form Fields:**
|
||||
- Description
|
||||
- Minimum amount
|
||||
- Maximum amount
|
||||
- Username (for Lightning address)
|
||||
- Allow comments toggle
|
||||
|
||||
### **TransactionList.vue** 📋
|
||||
Comprehensive transaction history display:
|
||||
|
||||
**Features:**
|
||||
- Chronological transaction listing
|
||||
- Real-time transaction updates
|
||||
- Transaction status indicators
|
||||
- Amount formatting with proper units
|
||||
- Transaction type differentiation (sent/received)
|
||||
- Fee information display
|
||||
|
||||
### **BalanceDisplay.vue** 💰
|
||||
Wallet balance component with real-time updates:
|
||||
|
||||
**Features:**
|
||||
- Real-time balance updates via WebSocket
|
||||
- Multiple unit display (sats, millisats)
|
||||
- Loading state indicators
|
||||
- Connection status awareness
|
||||
|
||||
## Payment Processing
|
||||
|
||||
### **Payment Flow Architecture**
|
||||
```
|
||||
User Action ──→ Validation ──→ Payment ──→ Confirmation ──→ UI Update
|
||||
│ │ │ │ │
|
||||
│ │ │ │ └─ Real-time via WebSocket
|
||||
│ │ │ └─ Transaction recorded
|
||||
│ │ └─ LNbits API call
|
||||
│ └─ Balance check, format validation
|
||||
└─ Send/Receive dialog interaction
|
||||
```
|
||||
|
||||
### **Payment Types Supported**
|
||||
|
||||
#### **Lightning Invoices (BOLT11)**
|
||||
```typescript
|
||||
// Direct invoice payment
|
||||
await walletService.sendPayment({
|
||||
destination: "lnbc1000n1p3xh4v...",
|
||||
amount: 1000 // Optional if invoice has amount
|
||||
})
|
||||
```
|
||||
|
||||
#### **Lightning Addresses**
|
||||
```typescript
|
||||
// Lightning address payment
|
||||
await walletService.sendPayment({
|
||||
destination: "user@stacker.news",
|
||||
amount: 1000,
|
||||
comment: "Great post!"
|
||||
})
|
||||
```
|
||||
|
||||
#### **LNURL Pay**
|
||||
```typescript
|
||||
// LNURL payment
|
||||
await walletService.sendPayment({
|
||||
destination: "LNURL1DP68GURN8GHJ7UM9WFMXJCM99E3K7MF0V9CXJ0M385EKVCENXC6R2C35XVUKXEFCV5MKVV34X5EKZD3EV56NYD3HXQURZEPEXEJXXEPNXSCRVWFNV9NXZCN9XQ6XYEFHVGCXXCMYXYMNSERXFQ5FNS",
|
||||
amount: 1000
|
||||
})
|
||||
```
|
||||
|
||||
### **Error Handling**
|
||||
Comprehensive error handling for payment failures:
|
||||
|
||||
- **Insufficient balance** - Clear messaging with current balance
|
||||
- **Invalid payment request** - Format validation errors
|
||||
- **Network failures** - Retry options and offline handling
|
||||
- **Payment routing failures** - Lightning network routing errors
|
||||
- **Timeout handling** - Payment timeout with status updates
|
||||
|
||||
## Configuration
|
||||
|
||||
### **Module Configuration**
|
||||
Configure the wallet module in `src/app.config.ts`:
|
||||
|
||||
```typescript
|
||||
modules: {
|
||||
wallet: {
|
||||
enabled: true,
|
||||
config: {
|
||||
// WebSocket configuration
|
||||
websocket: {
|
||||
enabled: true, // Enable real-time updates
|
||||
reconnectDelay: 1000, // Initial reconnection delay (ms)
|
||||
maxReconnectAttempts: 5 // Maximum reconnection attempts
|
||||
},
|
||||
|
||||
// Payment configuration
|
||||
payments: {
|
||||
defaultUnit: 'sats', // Default amount unit
|
||||
showFees: true, // Display fee information
|
||||
confirmLarge: 10000 // Confirm payments above this amount
|
||||
},
|
||||
|
||||
// UI configuration
|
||||
ui: {
|
||||
showBalance: true, // Display balance in UI
|
||||
showTransactions: true, // Show transaction history
|
||||
transactionLimit: 100 // Max transactions to display
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### **Environment Variables**
|
||||
Required environment setup:
|
||||
|
||||
```bash
|
||||
# LNbits server URL
|
||||
VITE_LNBITS_BASE_URL=https://your-lnbits-server.com
|
||||
|
||||
# Optional: Default wallet configuration
|
||||
VITE_DEFAULT_CURRENCY=sats
|
||||
VITE_PAYMENT_TIMEOUT=30000
|
||||
```
|
||||
|
||||
## Development Guide
|
||||
|
||||
### **Adding New Payment Types**
|
||||
To add support for new payment methods:
|
||||
|
||||
1. **Extend SendPaymentRequest interface:**
|
||||
```typescript
|
||||
interface SendPaymentRequest {
|
||||
destination: string
|
||||
amount: number
|
||||
comment?: string
|
||||
newPaymentType?: NewPaymentParams // Add new type
|
||||
}
|
||||
```
|
||||
|
||||
2. **Update WalletService.sendPayment():**
|
||||
```typescript
|
||||
async sendPayment(request: SendPaymentRequest): Promise<boolean> {
|
||||
// Add detection logic
|
||||
if (this.isNewPaymentType(request.destination)) {
|
||||
return this.handleNewPaymentType(request)
|
||||
}
|
||||
// ... existing logic
|
||||
}
|
||||
```
|
||||
|
||||
3. **Add validation and processing:**
|
||||
```typescript
|
||||
private isNewPaymentType(destination: string): boolean {
|
||||
// Detection logic
|
||||
}
|
||||
|
||||
private async handleNewPaymentType(request: SendPaymentRequest): Promise<boolean> {
|
||||
// Processing logic
|
||||
}
|
||||
```
|
||||
|
||||
### **Extending Transaction Types**
|
||||
To add new transaction types:
|
||||
|
||||
1. **Update PaymentTransaction interface:**
|
||||
```typescript
|
||||
interface PaymentTransaction {
|
||||
// ... existing fields
|
||||
type: 'sent' | 'received' | 'new_type'
|
||||
metadata?: NewTypeMetadata
|
||||
}
|
||||
```
|
||||
|
||||
2. **Update transaction mapping:**
|
||||
```typescript
|
||||
private mapPaymentToTransaction(payment: any): PaymentTransaction {
|
||||
// Add new type detection and mapping
|
||||
}
|
||||
```
|
||||
|
||||
### **Custom WebSocket Handlers**
|
||||
To add custom WebSocket message handling:
|
||||
|
||||
```typescript
|
||||
class WalletWebSocketService extends BaseService {
|
||||
private handleMessage(event: MessageEvent): void {
|
||||
const data = JSON.parse(event.data)
|
||||
|
||||
// Handle existing message types
|
||||
if (data.payment) {
|
||||
this.handlePaymentNotification(data.payment)
|
||||
}
|
||||
|
||||
// Add custom message handling
|
||||
if (data.custom_event) {
|
||||
this.handleCustomEvent(data.custom_event)
|
||||
}
|
||||
}
|
||||
|
||||
private handleCustomEvent(event: CustomEvent): void {
|
||||
// Custom event processing
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### **Testing Wallet Functionality**
|
||||
|
||||
#### **Unit Tests**
|
||||
```typescript
|
||||
describe('WalletService', () => {
|
||||
let service: WalletService
|
||||
|
||||
beforeEach(() => {
|
||||
service = new WalletService()
|
||||
})
|
||||
|
||||
it('should send Lightning payment', async () => {
|
||||
const request = {
|
||||
destination: 'lnbc1000n...',
|
||||
amount: 1000
|
||||
}
|
||||
|
||||
const result = await service.sendPayment(request)
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
#### **Integration Tests**
|
||||
```typescript
|
||||
describe('Wallet Integration', () => {
|
||||
it('should update balance after payment', async () => {
|
||||
const websocketService = new WalletWebSocketService()
|
||||
const paymentService = new PaymentService()
|
||||
|
||||
// Mock payment notification
|
||||
await websocketService.handlePaymentNotification({
|
||||
amount: 1000,
|
||||
wallet_balance: 5000
|
||||
})
|
||||
|
||||
expect(paymentService.totalBalance).toBe(5000000) // millisats
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
### Related Documentation
|
||||
- **[[../base-module/index|🏗️ Base Module]]** - Authentication and core services
|
||||
- **[[../../03-core-services/payment-service|💳 Payment Service]]** - Core payment processing
|
||||
- **[[websocket-integration|⚡ WebSocket Integration]]** - Real-time update details
|
||||
- **[[payment-processing|💸 Payment Processing]]** - Payment flow documentation
|
||||
|
||||
### API References
|
||||
- **[[../../05-api-reference/lnbits-api|⚡ LNbits API]]** - Backend API integration
|
||||
- **[[../../05-api-reference/lightning-payments|💰 Lightning Payments]]** - Payment protocol details
|
||||
|
||||
### Development Guides
|
||||
- **[[../../04-development/testing|🧪 Testing Guide]]** - Testing strategies
|
||||
- **[[../../04-development/debugging|🐛 Debugging]]** - Troubleshooting guide
|
||||
|
||||
---
|
||||
|
||||
**Tags:** #wallet #lightning #payments #websocket #real-time #module
|
||||
**Last Updated:** 2025-09-18
|
||||
**Author:** Development Team
|
||||
590
docs/02-modules/wallet-module/websocket-integration.md
Normal file
590
docs/02-modules/wallet-module/websocket-integration.md
Normal file
|
|
@ -0,0 +1,590 @@
|
|||
# ⚡ 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
|
||||
|
|
@ -80,6 +80,43 @@ interface AuthService {
|
|||
|
||||
**See:** [[authentication|📖 Authentication Service Documentation]]
|
||||
|
||||
### **PaymentService** 💳
|
||||
**Purpose:** Centralized Lightning payment processing and wallet management
|
||||
**Location:** `src/core/services/PaymentService.ts`
|
||||
**Token:** `SERVICE_TOKENS.PAYMENT_SERVICE`
|
||||
|
||||
**Key Features:**
|
||||
- **Wallet Management** - Multi-wallet support with preferred wallet selection
|
||||
- **Payment Processing** - Lightning invoices, QR code generation, and external wallet handling
|
||||
- **Balance Management** - Real-time wallet balance updates and tracking
|
||||
- **Payment Validation** - Balance checking and payment amount validation
|
||||
- **Error Handling** - Consistent payment error handling and user feedback
|
||||
|
||||
**Reactive State:**
|
||||
```typescript
|
||||
interface PaymentService {
|
||||
isProcessingPayment: ComputedRef<boolean> // Payment in progress
|
||||
paymentError: ComputedRef<string | null> // Payment error state
|
||||
totalBalance: number // Total balance across wallets (getter)
|
||||
hasWalletWithBalance: boolean // Wallet availability check (getter)
|
||||
}
|
||||
```
|
||||
|
||||
**Key Methods:**
|
||||
- `payWithWallet(paymentRequest, amount?, options?)` - Process Lightning payment
|
||||
- `generateQRCode(paymentRequest, options?)` - Generate payment QR codes
|
||||
- `openExternalWallet(paymentRequest)` - Launch external Lightning wallet
|
||||
- `updateWalletBalance(balanceMsat, walletId?)` - Update wallet balance from WebSocket
|
||||
- `getPreferredWallet()` - Get the primary wallet for operations
|
||||
- `getWalletWithBalance(requiredAmount?)` - Find wallet with sufficient balance
|
||||
|
||||
**Wallet Selection Logic:**
|
||||
- Always uses first wallet (`wallets[0]`) for consistency across modules
|
||||
- Provides helper methods for wallet selection and balance checking
|
||||
- Integrates with AuthService for wallet credentials and management
|
||||
|
||||
**See:** [[payment-service|📖 Payment Service Documentation]]
|
||||
|
||||
### **RelayHub** 🌐
|
||||
**Purpose:** Centralized Nostr relay connection management
|
||||
**Location:** `src/modules/base/nostr/relay-hub.ts`
|
||||
|
|
@ -222,6 +259,74 @@ interface EventBusEvents {
|
|||
|
||||
**See:** [[../01-architecture/event-bus|📖 Event Bus Communication Documentation]]
|
||||
|
||||
### **WalletService** 🏦
|
||||
**Purpose:** Wallet operations and transaction management
|
||||
**Location:** `src/modules/wallet/services/WalletService.ts`
|
||||
**Token:** `SERVICE_TOKENS.WALLET_SERVICE`
|
||||
|
||||
**Key Features:**
|
||||
- **Payment Operations** - Send Lightning payments to invoices, addresses, and LNURL
|
||||
- **Payment Link Management** - Create and manage LNURL payment links and Lightning addresses
|
||||
- **Transaction History** - Load and manage comprehensive transaction history
|
||||
- **Real-time Updates** - Add new transactions from WebSocket notifications
|
||||
|
||||
**Reactive State:**
|
||||
```typescript
|
||||
interface WalletService {
|
||||
transactions: ComputedRef<PaymentTransaction[]> // Transaction history
|
||||
payLinks: ComputedRef<PayLink[]> // Created payment links
|
||||
isCreatingPayLink: ComputedRef<boolean> // Pay link creation state
|
||||
isSendingPayment: ComputedRef<boolean> // Payment sending state
|
||||
error: ComputedRef<string | null> // Error state
|
||||
}
|
||||
```
|
||||
|
||||
**Key Methods:**
|
||||
- `sendPayment(request: SendPaymentRequest)` - Send Lightning payment
|
||||
- `createReceiveAddress(params)` - Create LNURL payment link with Lightning address
|
||||
- `deletePayLink(linkId: string)` - Remove payment link
|
||||
- `addTransaction(payment)` - Add transaction from WebSocket notification
|
||||
- `refresh()` - Reload transactions and payment links
|
||||
|
||||
### **WalletWebSocketService** ⚡
|
||||
**Purpose:** Real-time wallet balance updates via WebSocket
|
||||
**Location:** `src/modules/wallet/services/WalletWebSocketService.ts`
|
||||
**Token:** `SERVICE_TOKENS.WALLET_WEBSOCKET_SERVICE`
|
||||
|
||||
**Key Features:**
|
||||
- **Real-time Connection** - WebSocket connection to LNbits for instant updates
|
||||
- **Balance Synchronization** - Smart balance updates with unit conversion handling
|
||||
- **Payment Notifications** - Toast notifications for incoming payments
|
||||
- **Connection Management** - Automatic reconnection with exponential backoff
|
||||
- **Battery Optimization** - VisibilityService integration for mobile efficiency
|
||||
|
||||
**Reactive State:**
|
||||
```typescript
|
||||
interface WalletWebSocketService {
|
||||
isConnected: Ref<boolean> // WebSocket connection status
|
||||
connectionStatus: Ref<string> // Connection state details
|
||||
}
|
||||
```
|
||||
|
||||
**Connection States:**
|
||||
- `disconnected` - Not connected
|
||||
- `connecting` - Connection in progress
|
||||
- `connected` - Successfully connected
|
||||
- `reconnecting` - Attempting to reconnect
|
||||
- `error` - Connection failed
|
||||
- `failed` - Max reconnection attempts reached
|
||||
|
||||
**Key Methods:**
|
||||
- `reconnect()` - Manual reconnection trigger
|
||||
- `disconnect()` - Graceful disconnection
|
||||
- `cleanup()` - Service disposal
|
||||
|
||||
**WebSocket Integration:**
|
||||
- Connects to `wss://lnbits-server/api/v1/ws/{walletInkey}`
|
||||
- Handles incoming/outgoing payment balance differences
|
||||
- Updates PaymentService with converted balance (sats → millisats)
|
||||
- Integrates with WalletService for transaction updates
|
||||
|
||||
## Service Lifecycle
|
||||
|
||||
### **Initialization Phase**
|
||||
|
|
@ -479,9 +584,11 @@ describe('Service Integration', () => {
|
|||
|
||||
### Service Documentation
|
||||
- **[[authentication|🔐 Authentication Service]]** - User identity and session management
|
||||
- **[[payment-service|💳 Payment Service]]** - Lightning payment processing and wallet management
|
||||
- **[[storage-service|💾 Storage Service]]** - User-scoped data persistence
|
||||
- **[[toast-service|📢 Toast Service]]** - User notifications and feedback
|
||||
- **[[visibility-service|👁️ Visibility Service]]** - Dynamic UI component control
|
||||
- **[[../02-modules/wallet-module/index|💰 Wallet Services]]** - WalletService and WebSocket integration
|
||||
|
||||
### Architecture References
|
||||
- **[[../01-architecture/dependency-injection|⚙️ Dependency Injection]]** - DI container system
|
||||
|
|
|
|||
507
docs/03-core-services/payment-service.md
Normal file
507
docs/03-core-services/payment-service.md
Normal file
|
|
@ -0,0 +1,507 @@
|
|||
# 💳 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**
|
||||
```typescript
|
||||
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:
|
||||
```typescript
|
||||
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:
|
||||
```typescript
|
||||
// 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**
|
||||
```typescript
|
||||
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:
|
||||
|
||||
```typescript
|
||||
// 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**
|
||||
```typescript
|
||||
// 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:
|
||||
|
||||
```typescript
|
||||
// Called by WebSocket service when balance changes
|
||||
paymentService.updateWalletBalance(balanceMsat, walletId)
|
||||
```
|
||||
|
||||
### **Balance Update Logic**
|
||||
```typescript
|
||||
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:
|
||||
```typescript
|
||||
// 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:
|
||||
```typescript
|
||||
// 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**
|
||||
```typescript
|
||||
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**
|
||||
```typescript
|
||||
// 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**
|
||||
```typescript
|
||||
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**
|
||||
```typescript
|
||||
// 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**
|
||||
```typescript
|
||||
// WebSocket triggers payment notification
|
||||
eventBus.on('payment:received', (data) => {
|
||||
// PaymentService can listen and handle accordingly
|
||||
paymentService.handlePaymentNotification(data)
|
||||
})
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### **Public Methods**
|
||||
|
||||
#### **Payment Operations**
|
||||
```typescript
|
||||
// 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**
|
||||
```typescript
|
||||
// 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**
|
||||
```typescript
|
||||
// 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**
|
||||
```typescript
|
||||
// Clear payment error
|
||||
clearPaymentError(): void
|
||||
|
||||
// Reset payment state
|
||||
resetPaymentState(): void
|
||||
|
||||
// Force reset (for debugging)
|
||||
forceResetPaymentState(): void
|
||||
```
|
||||
|
||||
### **Reactive State**
|
||||
```typescript
|
||||
// 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**
|
||||
```vue
|
||||
<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 Module]]** - Wallet UI and WebSocket integration
|
||||
- **[[../02-modules/wallet-module/websocket-integration|⚡ WebSocket Integration]]** - Real-time balance updates
|
||||
- **[[authentication|🔐 Authentication Service]]** - User wallets and credentials
|
||||
|
||||
### API References
|
||||
- **[[../05-api-reference/lnbits-api|⚡ LNbits API]]** - Backend Lightning wallet API
|
||||
- **[[../05-api-reference/lightning-payments|💰 Lightning Payments]]** - Payment protocol details
|
||||
|
||||
### Development Guides
|
||||
- **[[../04-development/testing|🧪 Testing Guide]]** - Testing payment functionality
|
||||
- **[[../04-development/debugging|🐛 Debugging]]** - Payment troubleshooting
|
||||
|
||||
---
|
||||
|
||||
**Tags:** #payment #lightning #service #wallet #qr-code #lnbits
|
||||
**Last Updated:** 2025-09-18
|
||||
**Author:** Development Team
|
||||
|
|
@ -84,6 +84,7 @@ docs/
|
|||
|
||||
### Module Documentation
|
||||
- **[[02-modules/base-module/index|Base Module]]** - Core infrastructure (Nostr, Auth, PWA)
|
||||
- **[[02-modules/wallet-module/index|Wallet Module]]** - Lightning wallet with real-time WebSocket updates
|
||||
- **[[02-modules/market-module/index|Market Module]]** - Nostr marketplace implementation
|
||||
- **[[02-modules/chat-module/index|Chat Module]]** - Encrypted DM system
|
||||
- **[[02-modules/events-module/index|Events Module]]** - Lightning ticketing system
|
||||
|
|
@ -162,6 +163,7 @@ Content...
|
|||
| Module | Status | Coverage |
|
||||
|--------|--------|----------|
|
||||
| Base Module | ✅ Complete | 100% |
|
||||
| Wallet Module | ✅ Complete | 100% |
|
||||
| Market Module | ✅ Complete | 100% |
|
||||
| Chat Module | ✅ Complete | 100% |
|
||||
| Events Module | ✅ Complete | 100% |
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue