📄 src/web3/session/store.ts
D-OPEN SOVEREIGN

Documentation & Insights

Returns all known accounts that have completed on-chain registration
(i.e. `hasAccount === true`). This is a plain (non-reactive) snapshot —
wrap in `computed()` at the call site if reactivity is needed.
1import { type Web3SessionAccount, type SessionState, type SessionStore } from './types';
2
3export function createSessionStore(): SessionStore {
4  const PREFIX = '@the_library/web3-session:';
5  const NEW_SEG = 'infos_web3_';
6  const ACTIVE_KEY = 'active_session';
7
8  const state: SessionState = {
9    active: null,
10    known: [],
11  };
12
13  const changeListeners = new Set<() => void>();
14  const notify = () => changeListeners.forEach((l) => l());
15
16  const getLocalStorageKey = (address: string, chainId: number): string => {
17    return `${NEW_SEG}${chainId}_${address.toLowerCase()}`;
18  };
19
20  const persistAccount = (account: Web3SessionAccount) => {
21    if (typeof localStorage === 'undefined') return;
22    const key = getLocalStorageKey(account.account, account.chainId);
23    localStorage.setItem(PREFIX + key, JSON.stringify(account));
24  };
25
26  const removePersistedAccount = (address: string, chainId: number) => {
27    if (typeof localStorage === 'undefined') return;
28    const key = getLocalStorageKey(address, chainId);
29    localStorage.removeItem(PREFIX + key);
30  };
31
32  const persistActive = () => {
33    if (typeof localStorage === 'undefined') return;
34    if (state.active) {
35      localStorage.setItem(
36        PREFIX + ACTIVE_KEY,
37        JSON.stringify({ address: state.active.account, chainId: state.active.chainId })
38      );
39    } else {
40      localStorage.removeItem(PREFIX + ACTIVE_KEY);
41    }
42  };
43
44  const loadActive = (): Web3SessionAccount | null => {
45    if (typeof localStorage === 'undefined') return null;
46    const raw = localStorage.getItem(PREFIX + ACTIVE_KEY);
47    if (!raw) return null;
48    try {
49      const parsed = JSON.parse(raw);
50      return (
51        state.known.find(
52          (a) => a.account.toLowerCase() === parsed.address.toLowerCase() && a.chainId === parsed.chainId
53        ) || null
54      );
55    } catch {
56      return null;
57    }
58  };
59
60  function migrateStorageKeys(): void {
61    if (typeof localStorage === 'undefined') return;
62    const OLD_SEG = 'infos_evm_';
63    for (let i = 0; i < localStorage.length; i++) {
64      const raw = localStorage.key(i);
65      if (!raw) continue;
66      // Strip prefix to get the bare key, check segment
67      const bare = raw.startsWith(PREFIX) ? raw.slice(PREFIX.length) : null;
68      if (bare && bare.startsWith(OLD_SEG)) {
69        const newRaw = PREFIX + NEW_SEG + bare.slice(OLD_SEG.length);
70        localStorage.setItem(newRaw, localStorage.getItem(raw)!);
71        localStorage.removeItem(raw);
72        i--; // recheck index after removal
73      }
74    }
75  }
76
77  const store: SessionStore = {
78    getState() {
79      return state;
80    },
81    setActive(account: Web3SessionAccount | null) {
82      state.active = account;
83      if (account) {
84        // Add to known if not already present
85        const exists = state.known.some(
86          (a) => a.account.toLowerCase() === account.account.toLowerCase() && a.chainId === account.chainId
87        );
88        if (!exists) {
89          state.known.push(account);
90          persistAccount(account);
91        }
92      }
93      persistActive();
94      notify();
95    },
96    addKnown(account: Web3SessionAccount) {
97      const index = state.known.findIndex(
98        (a) => a.account.toLowerCase() === account.account.toLowerCase() && a.chainId === account.chainId
99      );
100      if (index >= 0) {
101        state.known[index] = account;
102      } else {
103        state.known.push(account);
104      }
105      persistAccount(account);
106      notify();
107    },
108    updateAccount(address: string, chainId: number, patch: Partial<Web3SessionAccount>) {
109      const account = state.known.find(
110        (a) => a.account.toLowerCase() === address.toLowerCase() && a.chainId === chainId
111      );
112      if (account) {
113        Object.assign(account, patch);
114        persistAccount(account);
115        if (
116          state.active &&
117          state.active.account.toLowerCase() === address.toLowerCase() &&
118          state.active.chainId === chainId
119        ) {
120          Object.assign(state.active, patch);
121          persistActive();
122        }
123        notify();
124      }
125    },
126    removeAccount(address: string, chainId: number) {
127      state.known = state.known.filter(
128        (a) => !(a.account.toLowerCase() === address.toLowerCase() && a.chainId === chainId)
129      );
130      removePersistedAccount(address, chainId);
131      if (
132        state.active &&
133        state.active.account.toLowerCase() === address.toLowerCase() &&
134        state.active.chainId === chainId
135      ) {
136        state.active = null;
137        persistActive();
138      }
139      notify();
140    },
141    clearAll() {
142      if (typeof localStorage !== 'undefined') {
143        const keysToRemove: string[] = [];
144        for (let i = 0; i < localStorage.length; i++) {
145          const key = localStorage.key(i);
146          if (key && key.startsWith(PREFIX)) {
147            keysToRemove.push(key);
148          }
149        }
150        for (const k of keysToRemove) {
151          localStorage.removeItem(k);
152        }
153      }
154      state.active = null;
155      state.known = [];
156      notify();
157    },
158    setLastSavedPatchIndex(address: string, chainId: number, index: number) {
159      this.updateAccount(address, chainId, { lastPatchIndex: index });
160    },
161    setLastRestorationIndex(address: string, chainId: number, index: number) {
162      this.updateAccount(address, chainId, { lastRestorationIndex: index });
163    },
164    lastSyncCounters(address: string, chainId: number) {
165      const account = state.known.find(
166        (a) => a.account.toLowerCase() === address.toLowerCase() && a.chainId === chainId
167      );
168      return {
169        lastPatchIndex: account?.lastPatchIndex ?? 0,
170        lastRestorationIndex: account?.lastRestorationIndex ?? 0,
171      };
172    },
173    updateProjectVotes(address: string, chainId: number, projectId: number, amount: number) {
174      const account = state.known.find(
175        (a) => a.account.toLowerCase() === address.toLowerCase() && a.chainId === chainId
176      );
177      if (account) {
178        const index = projectId - 1;
179        if (index < 0) return;
180        const votes = [...(account.donationsPerProject || [])];
181        while (votes.length <= index) {
182          votes.push(0);
183        }
184        votes[index] = (votes[index] || 0) + amount;
185        this.updateAccount(address, chainId, { donationsPerProject: votes });
186      }
187    },
188    persist() {
189      for (const a of state.known) {
190        persistAccount(a);
191      }
192      persistActive();
193    },
194    load() {
195      if (typeof localStorage === 'undefined') return;
196      migrateStorageKeys();
197
198      const loadedKnown: Web3SessionAccount[] = [];
199      for (let i = 0; i < localStorage.length; i++) {
200        const rawKey = localStorage.key(i);
201        if (rawKey && rawKey.startsWith(PREFIX + NEW_SEG)) {
202          try {
203            const rawVal = localStorage.getItem(rawKey);
204            if (rawVal) {
205              const account = JSON.parse(rawVal) as Web3SessionAccount;
206              loadedKnown.push(account);
207            }
208          } catch (e) {
209            console.warn('Failed to parse loaded session account:', e);
210          }
211        }
212      }
213      state.known = loadedKnown;
214      state.active = loadActive();
215      notify();
216    },
217    subscribe(listener: () => void) {
218      changeListeners.add(listener);
219      return () => changeListeners.delete(listener);
220    },
221  };
222
223  return store;
224}
225
226export const sessionStore: SessionStore = createSessionStore();
227
228/**
229 * Returns all known accounts that have completed on-chain registration
230 * (i.e. `hasAccount === true`). This is a plain (non-reactive) snapshot —
231 * wrap in `computed()` at the call site if reactivity is needed.
232 */
233export const allRegisteredAccounts = (): Web3SessionAccount[] =>
234  sessionStore.getState().known.filter((a) => a.hasAccount);
235