Documentation Index
Fetch the complete documentation index at: https://mintlify.com/Koniverse/SubWallet-Extension/llms.txt
Use this file to discover all available pages before exploring further.
What are Services?
Services are the core business logic layer in SubWallet Extension. They encapsulate complex functionality, manage state, handle API interactions, and coordinate data flow between different parts of the application.
Services provide:
- Lifecycle management with init, start, stop methods
- State management through RxJS observables
- Data persistence integration with stores and database
- Event-driven architecture for loosely coupled components
- Cron job support for periodic tasks
Service Interfaces
SubWallet defines several interfaces that services can implement:
CoreServiceInterface
The foundation for all services:
export interface CoreServiceInterface {
status: ServiceStatus;
init: () => Promise<void>;
startPromiseHandler: PromiseHandler<void>;
start: () => Promise<void>;
waitForStarted: () => Promise<void>;
}
StoppableServiceInterface
For services that need cleanup:
export interface StoppableServiceInterface extends CoreServiceInterface {
stopPromiseHandler: PromiseHandler<void>;
stop: () => Promise<void>;
waitForStopped: () => Promise<void>;
}
PersistDataServiceInterface
For services that manage persistent data:
export interface PersistDataServiceInterface {
loadData: () => Promise<void>;
persistData: () => Promise<void>;
}
CronServiceInterface
For services with periodic tasks:
export interface CronServiceInterface {
startCron: () => Promise<void>;
stopCron: () => Promise<void>;
}
Service Statuses
export enum ServiceStatus {
NOT_INITIALIZED = 'not_initialized',
INITIALIZING = 'initializing',
INITIALIZED = 'initialized',
STARTED = 'started',
STARTING = 'starting',
STARTED_FULL = 'started_full',
STARTING_FULL = 'starting_full',
STOPPED = 'stopped',
STOPPING = 'stopping',
}
Creating a New Service
Step 1: Define Your Service Structure
Create a new directory in packages/extension-base/src/services/:
services/
my-service/
index.ts # Main service class
types.ts # Service-specific types
helpers/ # Helper functions
index.ts
Step 2: Implement the Service Class
Here’s a complete example based on the PriceService:
// services/price-service/index.ts
import { ServiceStatus, StoppableServiceInterface, PersistDataServiceInterface, CronServiceInterface } from '../base/types';
import { ChainService } from '../chain-service';
import { EventService } from '../event-service';
import DatabaseService from '../storage-service/DatabaseService';
import { CurrentCurrencyStore } from '@subwallet/extension-base/stores';
import { createPromiseHandler } from '@subwallet/extension-base/utils/promise';
import { BehaviorSubject } from 'rxjs';
export class PriceService implements StoppableServiceInterface, PersistDataServiceInterface, CronServiceInterface {
status: ServiceStatus;
private dbService: DatabaseService;
private eventService: EventService;
private chainService: ChainService;
private priceSubject: BehaviorSubject<PriceJson>;
private refreshTimeout: NodeJS.Timeout | undefined;
private readonly currency = new CurrentCurrencyStore();
constructor (dbService: DatabaseService, eventService: EventService, chainService: ChainService) {
this.status = ServiceStatus.NOT_INITIALIZED;
this.dbService = dbService;
this.eventService = eventService;
this.chainService = chainService;
this.priceSubject = new BehaviorSubject({ /* default data */ });
// Auto-initialize
this.init().catch(console.error);
}
// Initialize service
async init (): Promise<void> {
this.status = ServiceStatus.INITIALIZING;
// Load persisted data
await this.loadData();
// Setup event listeners
this.eventService.on('asset.updateState', this.handleAssetUpdate);
this.status = ServiceStatus.INITIALIZED;
}
// Start service operations
startPromiseHandler = createPromiseHandler<void>();
async start (): Promise<void> {
if (this.status === ServiceStatus.STARTED) {
return;
}
try {
this.startPromiseHandler = createPromiseHandler<void>();
this.status = ServiceStatus.STARTING;
// Wait for dependencies
await this.eventService.waitAssetReady;
// Start cron jobs
await this.startCron();
this.status = ServiceStatus.STARTED;
this.startPromiseHandler.resolve();
} catch (e) {
this.startPromiseHandler.reject(e);
}
}
// Stop service operations
stopPromiseHandler = createPromiseHandler<void>();
async stop (): Promise<void> {
try {
this.status = ServiceStatus.STOPPING;
this.stopPromiseHandler = createPromiseHandler<void>();
await this.stopCron();
await this.persistData();
this.status = ServiceStatus.STOPPED;
this.stopPromiseHandler.resolve();
} catch (e) {
this.stopPromiseHandler.reject(e);
}
}
// Cron job management
async startCron (): Promise<void> {
this.refreshData();
}
async stopCron (): Promise<void> {
clearTimeout(this.refreshTimeout);
}
private refreshData = () => {
// Fetch latest data
this.fetchPriceData()
.then(() => this.priceSubject.next(/* updated data */))
.catch(console.error);
// Schedule next refresh
this.refreshTimeout = setTimeout(this.refreshData, 60000);
};
// Data persistence
async loadData (): Promise<void> {
const data = await this.dbService.getPriceStore();
this.priceSubject.next(data || DEFAULT_DATA);
}
async persistData (): Promise<void> {
await this.dbService.updatePriceStore(this.priceSubject.value);
}
// Public API
public getPriceSubject () {
return this.priceSubject;
}
async waitForStarted (): Promise<void> {
return this.startPromiseHandler.promise;
}
async waitForStopped (): Promise<void> {
return this.stopPromiseHandler.promise;
}
}
Step 3: Integrate with KoniState
Add your service to the main state handler:
// koni/background/handlers/State.ts
import { PriceService } from '@subwallet/extension-base/services/price-service';
export default class KoniState extends State {
public readonly priceService: PriceService;
constructor () {
super();
// Initialize service
this.priceService = new PriceService(
this.dbService,
this.eventService,
this.chainService
);
}
// Start all services
async startServices () {
await Promise.all([
this.priceService.start(),
// other services...
]);
}
// Stop all services
async stopServices () {
await Promise.all([
this.priceService.stop(),
// other services...
]);
}
}
Real-World Example: ChainService
The ChainService manages blockchain connections and chain metadata:
export class ChainService {
private dataMap: _DataMap = {
chainInfoMap: {},
chainStateMap: {},
assetRegistry: {},
assetRefMap: {}
};
private dbService: DatabaseService;
private eventService: EventService;
private substrateChainHandler: SubstrateChainHandler;
private evmChainHandler: EvmChainHandler;
constructor (dbService: DatabaseService, eventService: EventService) {
this.dbService = dbService;
this.eventService = eventService;
// Initialize chain handlers
this.substrateChainHandler = new SubstrateChainHandler();
this.evmChainHandler = new EvmChainHandler();
}
// Public methods
public getChainInfoMap (): Record<string, _ChainInfo> {
return this.dataMap.chainInfoMap;
}
public getAssetRegistry (): Record<string, _ChainAsset> {
return this.dataMap.assetRegistry;
}
public async enableChain (chainSlug: string): Promise<boolean> {
const chainInfo = this.dataMap.chainInfoMap[chainSlug];
if (!chainInfo) {
return false;
}
// Update state
this.dataMap.chainStateMap[chainSlug].active = true;
// Persist changes
await this.dbService.upsertChain(chainInfo);
// Emit event
this.eventService.emit('chain.updateState', chainSlug);
return true;
}
}
Service Communication Patterns
Pattern 1: Event-Driven Updates
export class BalanceService {
constructor (private eventService: EventService, private chainService: ChainService) {
// Listen to chain updates
this.eventService.on('chain.updateState', this.handleChainUpdate);
this.eventService.on('account.updateCurrent', this.handleAccountUpdate);
}
private handleChainUpdate = (chainSlug: string) => {
// Refresh balances for affected chain
this.refreshChainBalance(chainSlug);
};
private handleAccountUpdate = (account: AccountInfo) => {
// Refresh all balances for new account
this.refreshAllBalances(account.address);
};
}
Pattern 2: Observable State
export class PriceService {
private priceSubject: BehaviorSubject<PriceJson>;
public getPriceSubject () {
return this.priceSubject;
}
public subscribePrice (priceId: string, callback: (price: number) => void) {
return this.priceSubject.pipe(
map(data => data.priceMap[priceId]),
distinctUntilChanged()
).subscribe(callback);
}
}
Pattern 3: Service Dependencies
export class EarningService {
constructor (
private chainService: ChainService,
private balanceService: BalanceService,
private priceService: PriceService
) {
// Service depends on other services
}
async calculateEarnings (address: string): Promise<EarningInfo> {
const chains = this.chainService.getActiveChains();
const balances = await this.balanceService.getBalances(address);
const prices = this.priceService.getPriceSubject().value;
// Combine data from multiple services
return this.computeEarnings(chains, balances, prices);
}
}
Best Practices
1. Lazy Initialization
// Good: Wait for dependencies
async start (): Promise<void> {
await this.eventService.waitAssetReady;
await this.chainService.waitForReady();
// Now safe to start
}
// Bad: Start without dependencies
async start (): Promise<void> {
// Might fail if dependencies not ready
this.fetchData();
}
2. Graceful Shutdown
async stop (): Promise<void> {
// 1. Stop accepting new work
this.status = ServiceStatus.STOPPING;
// 2. Stop background tasks
await this.stopCron();
// 3. Persist important data
await this.persistData();
// 4. Cleanup resources
this.cleanup();
// 5. Mark as stopped
this.status = ServiceStatus.STOPPED;
}
3. Error Handling
private async fetchData () {
try {
const data = await this.apiCall();
this.dataSubject.next(data);
} catch (error) {
console.error('Failed to fetch data:', error);
// Don't crash the service, use cached data
this.dataSubject.next(this.cachedData);
}
}
4. Memory Management
export class MyService {
private subscriptions: Subscription[] = [];
async start () {
// Store subscriptions for cleanup
this.subscriptions.push(
this.eventService.on('event', this.handler)
);
}
async stop () {
// Cleanup subscriptions
this.subscriptions.forEach(sub => sub.unsubscribe());
this.subscriptions = [];
}
}
5. Testing Services
describe('PriceService', () => {
let service: PriceService;
let mockDb: DatabaseService;
let mockEvents: EventService;
beforeEach(() => {
mockDb = createMockDatabase();
mockEvents = createMockEventService();
service = new PriceService(mockDb, mockEvents, mockChainService);
});
afterEach(async () => {
await service.stop();
});
it('should load persisted data on init', async () => {
await service.init();
const data = service.getPriceSubject().value;
expect(data).toBeDefined();
});
});
Service Lifecycle
Common Service Types
Data Fetching Services
- PriceService: Fetches and caches token prices
- BalanceService: Manages account balances across chains
- NftService: Handles NFT metadata and ownership
State Management Services
- ChainService: Manages chain configurations and connections
- KeyringService: Handles account keys and signing
- SettingService: Manages user preferences
Background Processing Services
- HistoryService: Processes transaction history
- EarningService: Calculates staking rewards
- NotificationService: Manages in-app notifications
- Stores - Persist service data
- APIs - Services use APIs to fetch data
- Cron Jobs - Schedule periodic service tasks