Skip to main content

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 Stores?

Stores are data persistence layers in SubWallet Extension that handle saving and retrieving data from Chrome’s local storage. They provide a clean abstraction over the browser storage API and enable reactive data patterns through RxJS subjects. Stores are essential for:
  • Persisting user data across browser sessions
  • Reactive data flow through observable patterns
  • Type-safe storage with TypeScript interfaces
  • Namespaced data to avoid key conflicts

Store Architecture

SubWallet implements two types of stores:

BaseStore

The foundation class that provides basic CRUD operations with Chrome storage:
export default abstract class BaseStore <T> {
  #prefix: string;

  constructor (prefix: string | null) {
    this.#prefix = prefix ? `${prefix}:` : '';
  }

  public get (_key: string, update: (value: T) => void): void
  public set (_key: string, value: T, update?: () => void): void
  public remove (_key: string, update?: () => void): void
  public all (update: (key: string, value: T) => void): void
  public allMap (update: (value: Record<string, T>) => void): void
}
Key Features:
  • Prefix-based namespacing to avoid storage key collisions
  • Callback-based async operations
  • Automatic error handling via chrome.runtime.lastError

SubscribableStore

Extends BaseStore with reactive capabilities using 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);
      update && update();
    });
  }

  public asyncGet = async (key: string): Promise<T> => {
    return new Promise((resolve) => {
      this.get(key, resolve);
    });
  };
}
Key Features:
  • RxJS Subject emits on every data change
  • Promise-based asyncGet for modern async/await syntax
  • Automatic notification to all subscribers when data changes

Creating a New Store

Step 1: Define Your Store Class

Create a new file in packages/extension-base/src/stores/:
// CurrentAccountStore.ts
import { EXTENSION_PREFIX } from '@subwallet/extension-base/defaults';
import SubscribableStore from '@subwallet/extension-base/stores/SubscribableStore';
import { CurrentAccountInfo } from '@subwallet/extension-base/types';

export default class CurrentAccountStore extends SubscribableStore<CurrentAccountInfo> {
  constructor () {
    super(EXTENSION_PREFIX ? `${EXTENSION_PREFIX}current_account` : null);
  }
}
Important:
  • Use SubscribableStore if you need reactive updates
  • Use BaseStore for simple key-value storage
  • Always prefix with EXTENSION_PREFIX for namespace isolation
  • Use descriptive, unique keys for your store

Step 2: Define the Data Type

Ensure your data type is defined in the appropriate types file:
// background/KoniTypes.ts
export interface CurrentAccountInfo {
  address: string;
  currentAccountType: AccountType;
  proxyId?: string;
}

Step 3: Integrate into KoniState

Add your store to the state management class:
// koni/background/handlers/State.ts
export default class KoniState extends State {
  private readonly currentAccountStore = new CurrentAccountStore();
  private currentAccountReady = false;

  // Setter method
  public setCurrentAccount (accountInfo: CurrentAccountInfo, callback?: () => void): void {
    this.currentAccountStore.set('CurrentAccount', accountInfo, () => {
      this.eventService.emit('account.updateCurrent', accountInfo);
      callback && callback();
    });
  }

  // Getter method
  public getCurrentAccount (update: (value: CurrentAccountInfo) => void): void {
    if (!this.currentAccountReady) {
      this.currentAccountStore.get('CurrentAccount', (value) => {
        this.currentAccountReady = true;
        update(value);
      });
    } else {
      this.currentAccountStore.get('CurrentAccount', update);
    }
  }

  // Subscribe method for reactive updates
  public subscribeCurrentAccount () {
    return this.currentAccountStore.getSubject();
  }
}

Step 4: Export Your Store

Add your store to the index file:
// stores/index.ts
export { default as CurrentAccountStore } from './CurrentAccountStore';

Real-World Example: SettingsStore

Here’s how the SettingsStore is implemented:
import { RequestSettingsType } from '@subwallet/extension-base/background/KoniTypes';
import { EXTENSION_PREFIX } from '@subwallet/extension-base/defaults';
import SubscribableStore from '@subwallet/extension-base/stores/SubscribableStore';

export default class SettingsStore extends SubscribableStore<RequestSettingsType> {
  constructor () {
    super(EXTENSION_PREFIX ? `${EXTENSION_PREFIX}settings` : null);
  }
}
Usage in KoniState:
private readonly settingsStore = new SettingsStore();

public getSettings (update: (value: RequestSettingsType) => void): void {
  this.settingsStore.get('Settings', update);
}

public setSettings (settings: RequestSettingsType, callback?: () => void): void {
  this.settingsStore.set('Settings', settings, callback);
}

public subscribeSettings () {
  return this.settingsStore.getSubject();
}

Best Practices

1. Choose the Right Base Class

  • Use SubscribableStore when:
    • UI needs real-time updates
    • Multiple components depend on the data
    • Data changes frequently
  • Use BaseStore when:
    • Data is read once at startup
    • Simple configuration storage
    • No need for reactive updates

2. Storage Keys

// Good: Descriptive and unique
super(EXTENSION_PREFIX ? `${EXTENSION_PREFIX}current_account` : null);

// Bad: Too generic, potential conflicts
super(EXTENSION_PREFIX ? `${EXTENSION_PREFIX}data` : null);

3. Type Safety

Always define proper TypeScript interfaces:
// Good
export interface PriceJson {
  currency: CurrencyType;
  priceMap: Record<string, number>;
  ready: boolean;
}

class PriceStore extends SubscribableStore<PriceJson> {}

// Bad
class PriceStore extends SubscribableStore<any> {}

4. Ready State Pattern

Implement ready flags to prevent redundant storage reads:
private priceStoreReady = false;

public getPrice (update: (value: PriceJson) => void): void {
  if (!this.priceStoreReady) {
    this.priceStore.get('Price', (value) => {
      this.priceStoreReady = true;
      update(value);
    });
  } else {
    this.priceStore.get('Price', update);
  }
}

5. Event Emission

Emit events when data changes to trigger dependent systems:
public setPrice (priceData: PriceJson, callback?: () => void): void {
  this.priceStore.set('Price', priceData, () => {
    this.eventService.emit('price.updated', priceData);
    callback && callback();
  });
}

6. Error Handling

BaseStore automatically logs errors, but add application-level handling:
public async loadSettings (): Promise<RequestSettingsType> {
  try {
    return await this.settingsStore.asyncGet('Settings');
  } catch (error) {
    console.error('Failed to load settings:', error);
    return DEFAULT_SETTINGS; // Fallback
  }
}

Common Patterns

Pattern 1: Cache with Store Backup

private cachedData: PriceJson | null = null;

public async getPrice (): Promise<PriceJson> {
  if (this.cachedData) {
    return this.cachedData;
  }
  
  this.cachedData = await this.priceStore.asyncGet('Price');
  return this.cachedData;
}

Pattern 2: Subscribe with Initial Value

public subscribePrice (callback: (value: PriceJson) => void) {
  // Send current value immediately
  this.getPrice(callback);
  
  // Subscribe to future changes
  return this.priceStore.getSubject().subscribe(callback);
}

Pattern 3: Batch Updates

public async updateMultipleSettings (updates: Partial<RequestSettingsType>) {
  const current = await this.settingsStore.asyncGet('Settings');
  const merged = { ...current, ...updates };
  
  return new Promise<void>((resolve) => {
    this.settingsStore.set('Settings', merged, resolve);
  });
}

Debugging Stores

View Storage Contents

In Chrome DevTools:
  1. Open Developer Tools > Application > Local Storage
  2. Look for keys prefixed with your EXTENSION_PREFIX
  3. Inspect values (stored as JSON)

Subscribe to Changes

// In console or debugging code
this.priceStore.getSubject().subscribe((value) => {
  console.log('Price updated:', value);
});

Common Issues

Issue: Data not persisting
// Wrong: Callback not executed
this.store.set('key', value); // No callback

// Right: Wait for persistence
this.store.set('key', value, () => console.log('Saved'));
Issue: Subject not emitting
// Wrong: Using BaseStore instead of SubscribableStore
class MyStore extends BaseStore<T> {}

// Right: Use SubscribableStore for reactive updates
class MyStore extends SubscribableStore<T> {}

Store Lifecycle

  • Services - Use stores for service data persistence
  • APIs - Store API responses for caching
  • Cron Jobs - Periodically update store data

Build docs developers (and LLMs) love