- 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.
20 KiB
⚙️ Core Services Overview
Shared infrastructure services providing foundational functionality across all modules with reactive architecture, dependency injection, and lifecycle management.
Table of Contents
- #Service Architecture
- #Available Services
- #Service Lifecycle
- #Dependency Injection
- #Service Development
- #Testing Services
Service Architecture
BaseService Foundation
All core services extend the BaseService abstract class which provides:
- Reactive State Management - Integration with Vue's reactivity system
- Lifecycle Management - Standardized initialization and disposal
- Error Handling - Consistent error patterns across services
- Type Safety - Full TypeScript support with strict typing
abstract class BaseService {
protected isInitialized = ref(false)
protected isDisposed = ref(false)
abstract initialize(): Promise<void>
abstract dispose(): Promise<void>
// Reactive state helpers
protected createReactiveState<T>(initialValue: T): Ref<T>
protected createComputedState<T>(getter: () => T): ComputedRef<T>
}
Service Registration Pattern
Services are registered in the dependency injection container during module installation:
// Service registration (in base module)
container.provide(SERVICE_TOKENS.RELAY_HUB, relayHub)
container.provide(SERVICE_TOKENS.AUTH_SERVICE, authService)
container.provide(SERVICE_TOKENS.STORAGE_SERVICE, storageService)
// Service consumption (anywhere in app)
const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB)
const auth = injectService(SERVICE_TOKENS.AUTH_SERVICE)
Available Services
AuthService 🔐
Purpose: User authentication and identity management
Location: src/modules/base/auth/auth-service.ts
Token: SERVICE_TOKENS.AUTH_SERVICE
Key Features:
- Key Management - Secure generation, import, and storage of Nostr keys
- User Sessions - Persistent authentication with encrypted storage
- Profile Management - User profile creation and updates
- Security - Client-side key handling with no server storage
Reactive State:
interface AuthService {
user: Ref<NostrUser | null> // Current authenticated user
isAuthenticated: ComputedRef<boolean> // Authentication status
isLoading: Ref<boolean> // Loading state
loginError: Ref<string | null> // Login error message
}
Key Methods:
generateKeyPair()- Create new Nostr key pairloginWithPrivateKey(privateKey: string)- Authenticate with existing keylogout()- Clear session and user dataupdateProfile(profile: UserMetadata)- Update user profile
See: authentication
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:
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 paymentgenerateQRCode(paymentRequest, options?)- Generate payment QR codesopenExternalWallet(paymentRequest)- Launch external Lightning walletupdateWalletBalance(balanceMsat, walletId?)- Update wallet balance from WebSocketgetPreferredWallet()- Get the primary wallet for operationsgetWalletWithBalance(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
RelayHub 🌐
Purpose: Centralized Nostr relay connection management
Location: src/modules/base/nostr/relay-hub.ts
Token: SERVICE_TOKENS.RELAY_HUB
Key Features:
- Connection Management - Automatic connection, reconnection, and failover
- Event Publishing - Reliable event publishing across multiple relays
- Subscription Management - Efficient event subscriptions with deduplication
- Performance Monitoring - Relay latency and success rate tracking
Reactive State:
interface RelayHub {
connectedRelays: Ref<string[]> // Currently connected relays
connectionStatus: ComputedRef<ConnectionStatus> // Overall connection status
relayStats: Ref<Map<string, RelayStats>> // Per-relay statistics
isConnecting: Ref<boolean> // Connection in progress
}
Key Methods:
connect(relays: string[])- Connect to relay URLspublishEvent(event: NostrEvent)- Publish event to all connected relayssubscribe(filters: Filter[], callback: EventCallback)- Subscribe to eventsgetRelayInfo(url: string)- Get relay connection information
See: ../01-architecture/relay-hub
StorageService 💾
Purpose: User-scoped local storage operations
Location: src/core/services/StorageService.ts
Token: SERVICE_TOKENS.STORAGE_SERVICE
Key Features:
- User-Scoped Storage - Automatic key prefixing per authenticated user
- Type-Safe Operations - Strongly typed get/set operations with JSON serialization
- Reactive Updates - Optional reactive storage with Vue refs
- Migration Support - Data migration between storage schema versions
Key Methods:
interface StorageService {
setUserData<T>(key: string, data: T): void
getUserData<T>(key: string, defaultValue?: T): T | undefined
removeUserData(key: string): void
clearUserData(): void
// Reactive variants
getReactiveUserData<T>(key: string, defaultValue: T): Ref<T>
setReactiveUserData<T>(key: string, ref: Ref<T>): void
}
Storage Patterns:
- User-specific keys:
user:{pubkey}:settings - Global application keys:
app:theme - Module-specific keys:
user:{pubkey}:chat:contacts
See: storage-service
ToastService 📢
Purpose: Application-wide notifications and user feedback
Location: src/core/services/ToastService.ts
Token: SERVICE_TOKENS.TOAST_SERVICE
Key Features:
- Context-Specific Methods - Pre-configured toasts for common scenarios
- Consistent Messaging - Standardized success, error, and info messages
- Accessibility - Screen reader compatible notifications
- Customizable - Support for custom toast content and actions
Organized by Context:
interface ToastService {
// Authentication context
auth: {
loginSuccess(): void
loginError(error?: string): void
logoutSuccess(): void
keyGenerated(): void
}
// Payment context
payment: {
invoiceCreated(): void
paymentReceived(): void
paymentFailed(error?: string): void
}
// Clipboard operations
clipboard: {
copied(item?: string): void
copyFailed(): void
}
// General operations
operation: {
success(message: string): void
error(message: string): void
info(message: string): void
}
}
See: toast-service
EventBus 📡
Purpose: Inter-module communication and event coordination
Location: src/core/services/EventBus.ts
Token: SERVICE_TOKENS.EVENT_BUS
Key Features:
- Type-Safe Events - Strongly typed event payloads
- Module Isolation - Clean communication between modules
- Event Namespacing - Organized event names by domain
- Subscription Management - Easy subscribe/unsubscribe patterns
Event Categories:
interface EventBusEvents {
// User events
'user:authenticated': { userId: string, profile: UserMetadata }
'user:profile-updated': { userId: string, changes: Partial<UserMetadata> }
'user:logout': { userId: string }
// Chat events
'chat:message-received': { messageId: string, from: string, content: string }
'chat:typing-start': { from: string, chatId: string }
// Payment events
'payment:invoice-created': { invoiceId: string, amount: number }
'payment:received': { invoiceId: string, amount: number }
// Relay events
'relay:connected': { url: string }
'relay:disconnected': { url: string, reason?: string }
}
See: ../01-architecture/event-bus
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:
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 paymentcreateReceiveAddress(params)- Create LNURL payment link with Lightning addressdeletePayLink(linkId: string)- Remove payment linkaddTransaction(payment)- Add transaction from WebSocket notificationrefresh()- 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:
interface WalletWebSocketService {
isConnected: Ref<boolean> // WebSocket connection status
connectionStatus: Ref<string> // Connection state details
}
Connection States:
disconnected- Not connectedconnecting- Connection in progressconnected- Successfully connectedreconnecting- Attempting to reconnecterror- Connection failedfailed- Max reconnection attempts reached
Key Methods:
reconnect()- Manual reconnection triggerdisconnect()- Graceful disconnectioncleanup()- 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
Services are initialized in dependency order during application startup:
// 1. Base services (no dependencies)
await authService.initialize()
await storageService.initialize()
// 2. Infrastructure services (depend on base services)
await relayHub.initialize()
await toastService.initialize()
// 3. Feature services (depend on infrastructure)
await chatService.initialize()
await eventsService.initialize()
Service Dependencies
Services declare their dependencies through constructor injection:
class ChatService extends BaseService {
constructor(
private auth = injectService(SERVICE_TOKENS.AUTH_SERVICE),
private relayHub = injectService(SERVICE_TOKENS.RELAY_HUB),
private storage = injectService(SERVICE_TOKENS.STORAGE_SERVICE),
private eventBus = injectService(SERVICE_TOKENS.EVENT_BUS)
) {
super()
}
}
Disposal Phase
Services are disposed in reverse dependency order during application shutdown:
async dispose(): Promise<void> {
// Clean up subscriptions
this.subscriptions.forEach(sub => sub.close())
// Save persistent state
await this.storage.setUserData('chat:messages', this.messages.value)
// Mark as disposed
this.isDisposed.value = true
}
Dependency Injection
Service Tokens
Type-safe service tokens prevent runtime errors and enable proper TypeScript inference:
export const SERVICE_TOKENS = {
AUTH_SERVICE: Symbol('AUTH_SERVICE') as InjectionKey<AuthService>,
RELAY_HUB: Symbol('RELAY_HUB') as InjectionKey<RelayHub>,
STORAGE_SERVICE: Symbol('STORAGE_SERVICE') as InjectionKey<StorageService>,
TOAST_SERVICE: Symbol('TOAST_SERVICE') as InjectionKey<ToastService>,
} as const
Service Registration
Services are registered during module installation:
// In base module installation
export async function installBaseModule(app: App) {
// Create service instances
const authService = new AuthService()
const relayHub = new RelayHub()
const storageService = new StorageService()
// Register in container
container.provide(SERVICE_TOKENS.AUTH_SERVICE, authService)
container.provide(SERVICE_TOKENS.RELAY_HUB, relayHub)
container.provide(SERVICE_TOKENS.STORAGE_SERVICE, storageService)
// Initialize services
await authService.initialize()
await relayHub.initialize()
}
Service Consumption
Services are injected where needed using type-safe injection:
// In composables
export function useAuth() {
const authService = injectService(SERVICE_TOKENS.AUTH_SERVICE)
return {
user: authService.user,
login: authService.login,
logout: authService.logout
}
}
// In components
<script setup>
const toast = injectService(SERVICE_TOKENS.TOAST_SERVICE)
const handleSuccess = () => toast.operation.success('Action completed!')
</script>
Service Development
Creating a New Service
1. Service Class Implementation
// src/core/services/MyService.ts
export class MyService extends BaseService {
// Reactive state
private readonly _data = ref<MyData[]>([])
private readonly _isLoading = ref(false)
// Public readonly access to state
public readonly data = readonly(this._data)
public readonly isLoading = readonly(this._isLoading)
constructor(
private dependency = injectService(SERVICE_TOKENS.DEPENDENCY)
) {
super()
}
async initialize(): Promise<void> {
// Initialization logic
await this.loadInitialData()
this.isInitialized.value = true
}
async dispose(): Promise<void> {
// Cleanup logic
this._data.value = []
this.isDisposed.value = true
}
// Public methods
async createItem(item: CreateItemRequest): Promise<MyData> {
this._isLoading.value = true
try {
const newItem = await this.dependency.create(item)
this._data.value.push(newItem)
return newItem
} finally {
this._isLoading.value = false
}
}
}
2. Service Token Registration
// Add to SERVICE_TOKENS
export const SERVICE_TOKENS = {
// ... existing tokens
MY_SERVICE: Symbol('MY_SERVICE') as InjectionKey<MyService>,
} as const
3. Service Registration in Module
// In module installation
const myService = new MyService()
container.provide(SERVICE_TOKENS.MY_SERVICE, myService)
await myService.initialize()
Service Best Practices
Reactive State Management
- Use
ref()for mutable state,readonly()for public access - Provide computed properties for derived state
- Use
watch()andwatchEffect()for side effects
Error Handling
- Throw descriptive errors with proper types
- Use try/catch blocks with proper cleanup
- Log errors appropriately for debugging
Performance Optimization
- Implement proper subscription cleanup in
dispose() - Use debouncing for frequent operations
- Cache expensive computations with
computed()
Testing Services
Service Unit Tests
// tests/unit/services/MyService.test.ts
describe('MyService', () => {
let service: MyService
let mockDependency: MockType<DependencyService>
beforeEach(() => {
// Setup mocks
mockDependency = createMockService()
// Create service with mocks
service = new MyService(mockDependency)
})
afterEach(async () => {
await service.dispose()
})
it('should initialize correctly', async () => {
await service.initialize()
expect(service.isInitialized.value).toBe(true)
expect(service.data.value).toEqual([])
})
it('should create items', async () => {
await service.initialize()
const item = await service.createItem({ name: 'test' })
expect(service.data.value).toContain(item)
expect(mockDependency.create).toHaveBeenCalledWith({ name: 'test' })
})
})
Integration Tests
// tests/integration/services/ServiceIntegration.test.ts
describe('Service Integration', () => {
let container: DIContainer
beforeEach(async () => {
container = createTestContainer()
await installTestServices(container)
})
it('should handle cross-service communication', async () => {
const authService = container.get(SERVICE_TOKENS.AUTH_SERVICE)
const chatService = container.get(SERVICE_TOKENS.CHAT_SERVICE)
await authService.login('test-key')
const message = await chatService.sendMessage('Hello')
expect(message.author).toBe(authService.user.value?.pubkey)
})
})
See Also
Service Documentation
- authentication - User identity and session management
- payment-service - Lightning payment processing and wallet management
- storage-service - User-scoped data persistence
- toast-service - User notifications and feedback
- visibility-service - Dynamic UI component control
- ../02-modules/wallet-module/index - WalletService and WebSocket integration
Architecture References
- ../01-architecture/dependency-injection - DI container system
- ../01-architecture/event-bus - Inter-service messaging
- ../02-modules/index - How services integrate with modules
- ../04-development/testing - Service testing patterns
Tags: #services #architecture #dependency-injection #reactive-state
Last Updated: 2025-09-06
Author: Development Team