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.
Overview
SubWallet employs a multi-layer state management architecture:
- Chrome Storage Stores - Persistent storage for background service
- Redux Store - Client-side state management in UI
- Service State - In-memory state in background services
- Subscriptions - Real-time state synchronization
Architecture
Background Service
├── Chrome Storage (BaseStore/SubscribableStore)
│ ├── AccountsStore
│ ├── KeyringStore
│ ├── SettingsStore
│ └── ...
└── Service State (in-memory)
├── BalanceService
├── ChainService
└── ...
|
| Subscriptions
v
Extension UI
└── Redux Store
├── accountState
├── chainStore
├── balance
└── ...
Chrome Storage Stores
BaseStore
The foundation for all persistent storage in the background.
Location: packages/extension-base/src/stores/Base.ts
export default abstract class BaseStore<T> {
#prefix: string;
constructor(prefix: string | null) {
this.#prefix = prefix ? `${prefix}:` : '';
}
public getPrefix(): string {
return this.#prefix;
}
// Get all items
public all(update: (key: string, value: T) => void): void {
this.allMap((map): void => {
Object.entries(map).forEach(([key, value]): void => {
update(key, value);
});
});
}
// Get all items as map
public allMap(update: (value: Record<string, T>) => void): void {
chrome.storage.local.get(null, (result: StoreValue): void => {
const entries = Object.entries(result);
const map: Record<string, T> = {};
for (let i = 0; i < entries.length; i++) {
const [key, value] = entries[i];
if (key.startsWith(this.#prefix)) {
map[key.replace(this.#prefix, '')] = value as T;
}
}
update(map);
});
}
// Get single item
public get(_key: string, update: (value: T) => void): void {
const key = `${this.#prefix}${_key}`;
chrome.storage.local.get([key], (result: StoreValue): void => {
update(result[key] as T);
});
}
// Set item
public set(_key: string, value: T, update?: () => void): void {
const key = `${this.#prefix}${_key}`;
chrome.storage.local.set({ [key]: value }, (): void => {
update && update();
});
}
// Remove item
public remove(_key: string, update?: () => void): void {
const key = `${this.#prefix}${_key}`;
chrome.storage.local.remove(key, (): void => {
update && update();
});
}
}
Source: packages/extension-base/src/stores/Base.ts:14-81
Key Features:
- Namespaced keys with prefix
- Async callback-based API
- Chrome storage.local backend
- Type-safe value storage
SubscribableStore
Extends BaseStore with RxJS subscription support.
Location: packages/extension-base/src/stores/SubscribableStore.ts
import BaseStore from '@subwallet/extension-base/stores/Base';
import { Subject } from 'rxjs';
export default abstract class SubscribableStore<T> extends BaseStore<T> {
private readonly subject: Subject<T> = new Subject<T>();
public getSubject(): Subject<T> {
return this.subject;
}
public override set(_key: string, value: T, update?: () => void): void {
super.set(_key, value, () => {
this.subject.next(value); // Emit update
update && update();
});
}
public asyncGet = async (key: string): Promise<T> => {
return new Promise((resolve) => {
this.get(key, resolve);
});
};
public removeAll() {
return this.all((key) => this.remove(key));
}
}
Source: packages/extension-base/src/stores/SubscribableStore.ts:7-30
Key Features:
- RxJS Subject for reactive updates
- Emits on every
set() call
- Promise-based
asyncGet() helper
- Batch removal support
Built-in Stores
Location: packages/extension-base/src/stores/
// Account management
export class AccountsStore extends BaseStore<KeyringJson> {
constructor() {
super(EXTENSION_PREFIX ? `${EXTENSION_PREFIX}accounts` : null);
}
public override set(key: string, value: KeyringJson, update?: () => void): void {
// Skip testing accounts
if (key.startsWith('account:') && value.meta && value.meta.isTesting) {
update && update();
return;
}
super.set(key, value, update);
}
}
// Keyring passwords
export class KeyringStore extends BaseStore<string> {
constructor() {
super(EXTENSION_PREFIX ? `${EXTENSION_PREFIX}keyring` : null);
}
}
// Current account
export class CurrentAccountStore extends SubscribableStore<CurrentAccountInfo> {
constructor() {
super('current-account');
}
}
// Settings
export class SettingsStore extends SubscribableStore<UiSettings> {
constructor() {
super('settings');
}
}
// Chain metadata
export class MetadataStore extends BaseStore<MetadataDef> {
constructor() {
super(EXTENSION_PREFIX ? `${EXTENSION_PREFIX}metadata` : null);
}
}
// Authorization
export class AuthorizeStore extends BaseStore<AuthUrls> {
constructor() {
super(EXTENSION_PREFIX ? `${EXTENSION_PREFIX}auth` : null);
}
}
Source: packages/extension-base/src/stores/Accounts.ts:9-24
Redux Store (UI)
Store Configuration
Location: packages/extension-koni-ui/src/stores/index.ts
import { combineReducers, configureStore } from '@reduxjs/toolkit';
import { persistReducer, persistStore } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
const persistConfig = {
key: 'root',
version: 1,
storage: storage,
whitelist: [
'settings',
'uiViewState',
'staking',
'campaign',
'buyService',
'staticContent',
'price',
'earning'
]
};
const rootReducers = combineReducers({
// Feature stores
transactionHistory: TransactionHistoryReducer,
crowdloan: CrowdloanReducer,
nft: NftReducer,
staking: StakingReducer,
price: PriceReducer,
balance: BalanceReducer,
bonding: BondingReducer,
mantaPay: MantaPayReducer,
campaign: CampaignReducer,
buyService: BuyServiceReducer,
earning: EarningReducer,
swap: SwapReducer,
// Common stores
chainStore: ChainStoreReducer,
assetRegistry: AssetRegistryReducer,
// Base stores
requestState: RequestStateReducer,
settings: SettingsReducer,
accountState: AccountStateReducer,
uiViewState: UIViewStateReducer,
staticContent: StaticContentReducer,
// Other stores
walletConnect: WalletConnectReducer,
missionPool: MissionPoolReducer,
notification: NotificationReducer,
openGov: GovernanceReducer,
multisig: MultisigReducer
});
const persistedReducer = persistReducer(persistConfig, rootReducers);
export const store = configureStore({
devTools: process.env.NODE_ENV !== 'production',
reducer: persistedReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER]
}
})
});
export const persistor = persistStore(store);
export type RootState = ReturnType<typeof store.getState>;
export type StoreName = keyof RootState;
export type AppStore = typeof store;
export type AppDispatch = typeof store.dispatch;
Source: packages/extension-koni-ui/src/stores/index.ts:4-110
Store Categories
Base Stores
accountState: Account list, current account
settings: UI settings, theme, language
requestState: Authorization, signing requests
uiViewState: UI state, modals, navigation
staticContent: Static content, campaigns
Feature Stores
balance: Account balances
price: Token prices
transactionHistory: Transaction history
staking: Staking positions
earning: Yield positions
nft: NFT collections and items
swap: Swap pairs and quotes
crowdloan: Crowdloan contributions
Common Stores
chainStore: Chain info, metadata, state
assetRegistry: Token/asset registry
Example slice structure:
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
interface AccountState {
accounts: AccountJson[];
currentAccount: AccountJson | null;
isReady: boolean;
}
const initialState: AccountState = {
accounts: [],
currentAccount: null,
isReady: false
};
const accountSlice = createSlice({
name: 'accountState',
initialState,
reducers: {
updateAccounts: (state, action: PayloadAction<AccountJson[]>) => {
state.accounts = action.payload;
state.isReady = true;
},
setCurrentAccount: (state, action: PayloadAction<AccountJson | null>) => {
state.currentAccount = action.payload;
},
reset: (state) => {
state.accounts = [];
state.currentAccount = null;
state.isReady = false;
}
}
});
export const { updateAccounts, setCurrentAccount, reset } = accountSlice.actions;
export default accountSlice.reducer;
State Synchronization
Background to UI Subscriptions
Location: packages/extension-koni-ui/src/stores/utils.ts
// Subscribe to account updates
export function subscribeAccountsData() {
return lazySubscribeMessage(
'pri(accounts.subscribe)',
null,
(data) => {
store.dispatch(updateAccounts(data));
},
(data) => {
store.dispatch(updateAccounts(data));
}
);
}
// Subscribe to balance updates
export function subscribeBalance() {
return lazySubscribeMessage(
'pri(balance.subscribe)',
null,
(data) => {
store.dispatch(updateBalance(data));
},
(data) => {
store.dispatch(updateBalance(data));
}
);
}
// Subscribe to chain info
export function subscribeChainInfoMap() {
return lazySubscribeMessage(
'pri(chainService.subscribeChainInfoMap)',
null,
(data) => {
store.dispatch(updateChainInfoMap(data));
},
(data) => {
store.dispatch(updateChainInfoMap(data));
}
);
}
DataContext Integration
Location: packages/extension-koni-ui/src/contexts/DataContext.tsx
export interface DataHandler {
name: string;
unsub?: () => void;
isSubscription?: boolean;
start: () => void;
isStarted?: boolean;
isStartImmediately?: boolean;
promise?: Promise<any>;
relatedStores: StoreName[];
}
const DataContext: DataContextType = {
handlerMap: {},
storeDependencies: {},
addHandler: function(item: DataHandler) {
const { name } = item;
item.isSubscription = !!item.unsub;
if (!this.handlerMap[name]) {
this.handlerMap[name] = item;
// Track dependencies
item.relatedStores.forEach((storeName) => {
if (!this.storeDependencies[storeName]) {
this.storeDependencies[storeName] = [];
}
this.storeDependencies[storeName]?.push(name);
});
// Auto-start if configured
if (item.isStartImmediately) {
item.start();
item.isStarted = true;
}
}
return () => this.removeHandler(name);
},
awaitStores: async function(storeNames: StoreName[]): Promise<boolean> {
// Wait for all required handlers to complete
const handlers = storeNames.flatMap(
(storeName) => this.storeDependencies[storeName] || []
);
const promises = handlers.map((name) => {
const handler = this.handlerMap[name];
if (!handler.isStarted) {
handler.start();
handler.isStarted = true;
}
return handler.promise;
});
await Promise.all(promises);
return true;
}
};
Source: packages/extension-koni-ui/src/contexts/DataContext.tsx:41-103
Common Patterns
Reading from Chrome Storage
// Callback-based
const accountStore = new AccountsStore();
accountStore.get('account:0x123', (account) => {
console.log(account);
});
// Promise-based (with SubscribableStore)
const settingsStore = new SettingsStore();
const settings = await settingsStore.asyncGet('default');
Writing to Chrome Storage
const accountStore = new AccountsStore();
accountStore.set('account:0x123', accountData, () => {
console.log('Account saved');
});
Subscribing to Changes
const currentAccountStore = new CurrentAccountStore();
const subject = currentAccountStore.getSubject();
const subscription = subject.subscribe((account) => {
console.log('Account changed:', account);
});
// Later
subscription.unsubscribe();
Using Redux Store in Components
import { useSelector, useDispatch } from 'react-redux';
import { RootState } from '@subwallet/extension-koni-ui/stores';
function MyComponent() {
const dispatch = useDispatch();
// Select state
const accounts = useSelector((state: RootState) => state.accountState.accounts);
const balance = useSelector((state: RootState) => state.balance);
// Dispatch actions
const handleUpdate = () => {
dispatch(updateAccounts(newAccounts));
};
return <div>{accounts.length} accounts</div>;
}
Awaiting Store Data
import { useContext, useEffect, useState } from 'react';
import { DataContext } from '@subwallet/extension-koni-ui/contexts/DataContext';
function MyComponent() {
const { awaitStores } = useContext(DataContext);
const [isReady, setIsReady] = useState(false);
useEffect(() => {
awaitStores(['accountState', 'chainStore', 'balance'])
.then(() => setIsReady(true));
}, []);
if (!isReady) {
return <LoadingScreen />;
}
return <MyContent />;
}
State Persistence
Chrome Storage (Background)
Persistent:
- Survives extension restart
- Survives browser restart
- Shared across all extension contexts
- Quota: ~5MB (local storage)
Use Cases:
- Account data
- Keyring
- Settings
- Authorization data
- Metadata
Redux Persist (UI)
Persistent (via localStorage):
- Survives page refresh
- Lost on extension update (sometimes)
- Separate per popup instance
Persisted Stores:
whitelist: [
'settings', // UI preferences
'uiViewState', // UI state
'staking', // Staking preferences
'campaign', // Campaign state
'buyService', // Buy service state
'staticContent', // Static content
'price', // Price cache
'earning' // Earning preferences
]
Not Persisted (ephemeral):
- Balance (refreshed on load)
- Transaction history (subscribed)
- NFTs (subscribed)
- Chain state (subscribed)
- Request state (ephemeral)
Best Practices
-
Use Appropriate Storage:
- Chrome Storage for critical data in background
- Redux for UI state
- Don’t duplicate data unnecessarily
-
Subscribe to Updates:
- Use SubscribableStore for reactive data
- Set up subscriptions in DataContext
- Clean up subscriptions on unmount
-
Await Store Readiness:
- Use
awaitStores() before rendering
- Show loading states while waiting
- Handle loading errors gracefully
-
Type Safety:
- Define TypeScript types for all state
- Use RootState type for selectors
- Use PayloadAction for Redux actions
-
Performance:
- Don’t persist large datasets
- Use memoization for expensive selectors
- Batch updates when possible
-
Error Handling:
- Handle Chrome storage errors
- Validate data on read
- Provide fallbacks for missing data